← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:black-charms into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:black-charms into launchpad:master.

Commit message:
lp.charms: Apply black

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/426189
-- 
The attached diff has been truncated due to its size.
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:black-charms into launchpad:master.
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index 1ab33b5..1a2bd19 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -68,3 +68,5 @@ c606443bdb2f342593c9a7c9437cb70c01f85f29
 2edd6d52841e03ba11ad431e40828f2f0b6a7e17
 # apply black to lp.buildmaster
 95cf83968d59453397dfbdcbe30556fc8004479a
+# apply black to lp.charms
+a6bed71f3d2fdbceae20c2d435c993e8bededdce
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index db91bfe..accf87d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -48,6 +48,7 @@ repos:
             |blueprints
             |bugs
             |buildmaster
+            |charms
           )/
 -   repo: https://github.com/PyCQA/isort
     rev: 5.9.2
@@ -72,6 +73,7 @@ repos:
             |blueprints
             |bugs
             |buildmaster
+            |charms
           )/
     -   id: isort
         alias: isort-black
@@ -86,6 +88,7 @@ repos:
             |blueprints
             |bugs
             |buildmaster
+            |charms
           )/
 -   repo: https://github.com/PyCQA/flake8
     rev: 3.9.2
diff --git a/lib/lp/charms/adapters/buildarch.py b/lib/lp/charms/adapters/buildarch.py
index bcb7cad..615a9f0 100644
--- a/lib/lp/charms/adapters/buildarch.py
+++ b/lib/lp/charms/adapters/buildarch.py
@@ -3,13 +3,10 @@
 
 __all__ = [
     "determine_instances_to_build",
-    ]
+]
 
-from collections import (
-    Counter,
-    OrderedDict,
-    )
 import json
+from collections import Counter, OrderedDict
 
 from lp.services.helpers import english_list
 
@@ -23,7 +20,8 @@ class MissingPropertyError(CharmBasesParserError):
 
     def __init__(self, prop):
         super().__init__(
-            "Base specification is missing the {!r} property".format(prop))
+            "Base specification is missing the {!r} property".format(prop)
+        )
         self.property = prop
 
 
@@ -38,7 +36,9 @@ class DuplicateRunOnError(CharmBasesParserError):
         super().__init__(
             "{} {} present in the 'run-on' of multiple items".format(
                 english_list([str(d) for d in duplicates]),
-                "is" if len(duplicates) == 1 else "are"))
+                "is" if len(duplicates) == 1 else "are",
+            )
+        )
 
 
 class CharmBase:
@@ -49,7 +49,9 @@ class CharmBase:
         if not isinstance(channel, str):
             raise BadPropertyError(
                 "Channel {!r} is not a string (missing quotes?)".format(
-                    channel))
+                    channel
+                )
+            )
         self.channel = channel
         self.architectures = architectures
 
@@ -65,26 +67,31 @@ class CharmBase:
         except KeyError:
             raise MissingPropertyError("channel")
         return cls(
-            name=name, channel=channel,
-            architectures=properties.get("architectures"))
+            name=name,
+            channel=channel,
+            architectures=properties.get("architectures"),
+        )
 
     def __eq__(self, other):
         return (
-            self.name == other.name and
-            self.channel == other.channel and
-            self.architectures == other.architectures)
+            self.name == other.name
+            and self.channel == other.channel
+            and self.architectures == other.architectures
+        )
 
     def __ne__(self, other):
         return not self == other
 
     def __hash__(self):
         architectures = (
-            None if self.architectures is None else tuple(self.architectures))
+            None if self.architectures is None else tuple(self.architectures)
+        )
         return hash((self.name, self.channel, architectures))
 
     def __str__(self):
         return "{} {} {}".format(
-            self.name, self.channel, json.dumps(self.architectures))
+            self.name, self.channel, json.dumps(self.architectures)
+        )
 
 
 class CharmBaseConfiguration:
@@ -101,8 +108,9 @@ class CharmBaseConfiguration:
         # common typos in case the user intends to use long-form but did so
         # incorrectly (for better error message handling).
         if not any(
-                item in properties
-                for item in ("run-on", "run_on", "build-on", "build_on")):
+            item in properties
+            for item in ("run-on", "run_on", "build-on", "build_on")
+        ):
             base = CharmBase.from_dict(properties)
             return cls([base], run_on=[base])
 
@@ -117,8 +125,9 @@ class CharmBaseConfiguration:
         return cls(build_on, run_on=run_on)
 
 
-def determine_instances_to_build(charmcraft_data, supported_arches,
-                                 default_distro_series):
+def determine_instances_to_build(
+    charmcraft_data, supported_arches, default_distro_series
+):
     """Return a list of instances to build based on charmcraft.yaml.
 
     :param charmcraft_data: A parsed charmcraft.yaml.
@@ -133,18 +142,24 @@ def determine_instances_to_build(charmcraft_data, supported_arches,
 
     if bases_list:
         configs = [
-            CharmBaseConfiguration.from_dict(item) for item in bases_list]
+            CharmBaseConfiguration.from_dict(item) for item in bases_list
+        ]
     else:
         # If no bases are specified, build one for each supported
         # architecture for the default series.
         configs = [
-            CharmBaseConfiguration([
-                CharmBase(
-                    default_distro_series.distribution.name,
-                    default_distro_series.version, das.architecturetag),
-                ])
+            CharmBaseConfiguration(
+                [
+                    CharmBase(
+                        default_distro_series.distribution.name,
+                        default_distro_series.version,
+                        das.architecturetag,
+                    ),
+                ]
+            )
             for das in supported_arches
-            if das.distroseries == default_distro_series]
+            if das.distroseries == default_distro_series
+        ]
 
     # Ensure that multiple `run-on` items don't overlap; this is ambiguous
     # and forbidden by charmcraft.
@@ -166,7 +181,9 @@ def determine_instances_to_build(charmcraft_data, supported_arches,
                 if das.distroseries.distribution.name != build_on.name:
                     continue
                 if build_on.channel not in (
-                        das.distroseries.name, das.distroseries.version):
+                    das.distroseries.name,
+                    das.distroseries.version,
+                ):
                     continue
                 if build_on.architectures is None:
                     # Build on all supported architectures for the requested
diff --git a/lib/lp/charms/adapters/tests/test_buildarch.py b/lib/lp/charms/adapters/tests/test_buildarch.py
index 6281def..c7659ba 100644
--- a/lib/lp/charms/adapters/tests/test_buildarch.py
+++ b/lib/lp/charms/adapters/tests/test_buildarch.py
@@ -3,98 +3,113 @@
 
 from functools import partial
 
-from testscenarios import (
-    load_tests_apply_scenarios,
-    WithScenarios,
-    )
+from testscenarios import WithScenarios, load_tests_apply_scenarios
 from testtools.matchers import (
     Equals,
     MatchesException,
     MatchesListwise,
     MatchesStructure,
     Raises,
-    )
+)
 from zope.component import getUtility
 
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.buildmaster.interfaces.processor import (
     IProcessorSet,
     ProcessorNotFound,
-    )
+)
 from lp.charms.adapters.buildarch import (
     CharmBase,
     CharmBaseConfiguration,
-    determine_instances_to_build,
     DuplicateRunOnError,
-    )
-from lp.testing import (
-    TestCase,
-    TestCaseWithFactory,
-    )
+    determine_instances_to_build,
+)
+from lp.testing import TestCase, TestCaseWithFactory
 from lp.testing.layers import LaunchpadZopelessLayer
 
 
 class TestCharmBaseConfiguration(WithScenarios, TestCase):
 
     scenarios = [
-        ("expanded", {
-            "base": {
-                "build-on": [{
-                    "name": "ubuntu",
-                    "channel": "18.04",
-                    "architectures": ["amd64"],
-                    }],
-                "run-on": [
-                    {
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64", "arm64"],
+        (
+            "expanded",
+            {
+                "base": {
+                    "build-on": [
+                        {
+                            "name": "ubuntu",
+                            "channel": "18.04",
+                            "architectures": ["amd64"],
+                        }
+                    ],
+                    "run-on": [
+                        {
+                            "name": "ubuntu",
+                            "channel": "20.04",
+                            "architectures": ["amd64", "arm64"],
                         },
-                    {
-                        "name": "ubuntu",
-                        "channel": "18.04",
-                        "architectures": ["amd64"],
+                        {
+                            "name": "ubuntu",
+                            "channel": "18.04",
+                            "architectures": ["amd64"],
                         },
                     ],
                 },
-            "expected_build_on": [
-                CharmBase(
-                    name="ubuntu", channel="18.04", architectures=["amd64"]),
+                "expected_build_on": [
+                    CharmBase(
+                        name="ubuntu", channel="18.04", architectures=["amd64"]
+                    ),
                 ],
-            "expected_run_on": [
-                CharmBase(
-                    name="ubuntu", channel="20.04",
-                    architectures=["amd64", "arm64"]),
-                CharmBase(
-                    name="ubuntu", channel="18.04", architectures=["amd64"]),
+                "expected_run_on": [
+                    CharmBase(
+                        name="ubuntu",
+                        channel="20.04",
+                        architectures=["amd64", "arm64"],
+                    ),
+                    CharmBase(
+                        name="ubuntu", channel="18.04", architectures=["amd64"]
+                    ),
                 ],
-            }),
-        ("short form", {
-            "base": {
-                "name": "ubuntu",
-                "channel": "20.04",
-                },
-            "expected_build_on": [CharmBase(name="ubuntu", channel="20.04")],
-            "expected_run_on": [CharmBase(name="ubuntu", channel="20.04")],
-            }),
-        ("no run-on", {
-            "base": {
-                "build-on": [{
+            },
+        ),
+        (
+            "short form",
+            {
+                "base": {
                     "name": "ubuntu",
                     "channel": "20.04",
-                    "architectures": ["amd64"],
-                    }],
                 },
-            "expected_build_on": [
-                CharmBase(
-                    name="ubuntu", channel="20.04", architectures=["amd64"]),
+                "expected_build_on": [
+                    CharmBase(name="ubuntu", channel="20.04")
                 ],
-            "expected_run_on": [
-                CharmBase(
-                    name="ubuntu", channel="20.04", architectures=["amd64"]),
+                "expected_run_on": [CharmBase(name="ubuntu", channel="20.04")],
+            },
+        ),
+        (
+            "no run-on",
+            {
+                "base": {
+                    "build-on": [
+                        {
+                            "name": "ubuntu",
+                            "channel": "20.04",
+                            "architectures": ["amd64"],
+                        }
+                    ],
+                },
+                "expected_build_on": [
+                    CharmBase(
+                        name="ubuntu", channel="20.04", architectures=["amd64"]
+                    ),
                 ],
-            }),
-        ]
+                "expected_run_on": [
+                    CharmBase(
+                        name="ubuntu", channel="20.04", architectures=["amd64"]
+                    ),
+                ],
+            },
+        ),
+    ]
 
     def test_base(self):
         config = CharmBaseConfiguration.from_dict(self.base)
@@ -109,230 +124,337 @@ class TestDetermineInstancesToBuild(WithScenarios, TestCaseWithFactory):
     # Scenarios taken from the charmcraft build providers specification:
     # https://discourse.charmhub.io/t/charmcraft-bases-provider-support/4713
     scenarios = [
-        ("single entry, single arch", {
-            "bases": [{
-                "build-on": [{
-                    "name": "ubuntu",
-                    "channel": "18.04",
-                    "architectures": ["amd64"],
-                    }],
-                "run-on": [{
-                    "name": "ubuntu",
-                    "channel": "18.04",
-                    "architectures": ["amd64"],
-                    }],
-                }],
-            "expected": [("18.04", "amd64")],
-            }),
-        ("multiple entries, single arch", {
-            "bases": [
-                {
-                    "build-on": [{
-                        "name": "ubuntu",
-                        "channel": "18.04",
-                        "architectures": ["amd64"],
-                        }],
-                    "run-on": [{
-                        "name": "ubuntu",
-                        "channel": "18.04",
-                        "architectures": ["amd64"],
-                        }],
+        (
+            "single entry, single arch",
+            {
+                "bases": [
+                    {
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "18.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "18.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                    }
+                ],
+                "expected": [("18.04", "amd64")],
+            },
+        ),
+        (
+            "multiple entries, single arch",
+            {
+                "bases": [
+                    {
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "18.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "18.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
                     },
-                {
-                    "build-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        }],
-                    "run-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        }],
+                    {
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
                     },
-                {
-                    "build-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["riscv64"],
-                        }],
-                    "run-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["riscv64"],
-                        }],
+                    {
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["riscv64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["riscv64"],
+                            }
+                        ],
                     },
                 ],
-            "expected": [
-                ("18.04", "amd64"), ("20.04", "amd64"), ("20.04", "riscv64")],
-            }),
-        ("single entry, multiple arches", {
-            "bases": [{
-                "build-on": [{
-                    "name": "ubuntu",
-                    "channel": "20.04",
-                    "architectures": ["amd64"],
-                    }],
-                "run-on": [{
-                    "name": "ubuntu",
-                    "channel": "20.04",
-                    "architectures": ["amd64", "riscv64"],
-                    }],
-                }],
-            "expected": [("20.04", "amd64")],
-            }),
-        ("multiple entries, with cross-arch", {
-            "bases": [
-                {
-                    "build-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        }],
-                    "run-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["riscv64"],
-                        }],
+                "expected": [
+                    ("18.04", "amd64"),
+                    ("20.04", "amd64"),
+                    ("20.04", "riscv64"),
+                ],
+            },
+        ),
+        (
+            "single entry, multiple arches",
+            {
+                "bases": [
+                    {
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64", "riscv64"],
+                            }
+                        ],
+                    }
+                ],
+                "expected": [("20.04", "amd64")],
+            },
+        ),
+        (
+            "multiple entries, with cross-arch",
+            {
+                "bases": [
+                    {
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["riscv64"],
+                            }
+                        ],
                     },
-                {
-                    "build-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        }],
-                    "run-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        }],
+                    {
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
                     },
                 ],
-            "expected": [("20.04", "amd64")],
-            }),
-        ("multiple run-on entries", {
-            "bases": [{
-                "build-on": [{
-                    "name": "ubuntu",
-                    "channel": "20.04",
-                    "architectures": ["amd64"],
-                    }],
-                "run-on": [
+                "expected": [("20.04", "amd64")],
+            },
+        ),
+        (
+            "multiple run-on entries",
+            {
+                "bases": [
                     {
-                        "name": "ubuntu",
-                        "channel": "18.04",
-                        "architectures": ["amd64"],
-                        },
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "18.04",
+                                "architectures": ["amd64"],
+                            },
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64", "riscv64"],
+                            },
+                        ],
+                    }
+                ],
+                "expected": [("20.04", "amd64")],
+            },
+        ),
+        (
+            "multiple build-on entries",
+            {
+                "bases": [
                     {
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64", "riscv64"],
-                        },
-                    ],
-                }],
-            "expected": [("20.04", "amd64")],
-            }),
-        ("multiple build-on entries", {
-            "bases": [{
-                "build-on": [
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "18.04",
+                                "architectures": ["amd64"],
+                            },
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            },
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                    }
+                ],
+                "expected": [("18.04", "amd64")],
+            },
+        ),
+        (
+            "redundant outputs",
+            {
+                "bases": [
                     {
-                        "name": "ubuntu",
-                        "channel": "18.04",
-                        "architectures": ["amd64"],
-                        },
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "18.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                    },
                     {
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        },
-                    ],
-                "run-on": [{
-                    "name": "ubuntu",
-                    "channel": "20.04",
-                    "architectures": ["amd64"],
-                    }],
-                }],
-            "expected": [("18.04", "amd64")],
-            }),
-        ("redundant outputs", {
-            "bases": [
-                {
-                    "build-on": [{
-                        "name": "ubuntu",
-                        "channel": "18.04",
-                        "architectures": ["amd64"],
-                        }],
-                    "run-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        }],
+                        "build-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
+                        "run-on": [
+                            {
+                                "name": "ubuntu",
+                                "channel": "20.04",
+                                "architectures": ["amd64"],
+                            }
+                        ],
                     },
-                {
-                    "build-on": [{
-                        "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        }],
-                    "run-on": [{
+                ],
+                "expected_exception": MatchesException(
+                    DuplicateRunOnError,
+                    r"ubuntu 20\.04 \[\"amd64\"\] is present in the 'run-on' "
+                    r"of multiple items",
+                ),
+            },
+        ),
+        (
+            "no bases specified",
+            {
+                "bases": None,
+                "expected": [
+                    ("20.04", "amd64"),
+                    ("20.04", "arm64"),
+                    ("20.04", "riscv64"),
+                ],
+            },
+        ),
+        (
+            "abbreviated, no architectures specified",
+            {
+                "bases": [
+                    {
                         "name": "ubuntu",
-                        "channel": "20.04",
-                        "architectures": ["amd64"],
-                        }],
-                    },
+                        "channel": "18.04",
+                    }
                 ],
-            "expected_exception": MatchesException(
-                DuplicateRunOnError,
-                r"ubuntu 20\.04 \[\"amd64\"\] is present in the 'run-on' of "
-                r"multiple items"),
-            }),
-        ("no bases specified", {
-            "bases": None,
-            "expected": [
-                ("20.04", "amd64"), ("20.04", "arm64"), ("20.04", "riscv64")],
-            }),
-        ("abbreviated, no architectures specified", {
-            "bases": [{
-                "name": "ubuntu",
-                "channel": "18.04",
-                }],
-            "expected": [
-                ("18.04", "amd64"), ("18.04", "arm64"), ("18.04", "riscv64")],
-            }),
-        ]
+                "expected": [
+                    ("18.04", "amd64"),
+                    ("18.04", "arm64"),
+                    ("18.04", "riscv64"),
+                ],
+            },
+        ),
+    ]
 
     def test_parser(self):
         distro_serieses = [
             self.factory.makeDistroSeries(
                 distribution=getUtility(ILaunchpadCelebrities).ubuntu,
-                version=version)
-            for version in ("20.04", "18.04")]
+                version=version,
+            )
+            for version in ("20.04", "18.04")
+        ]
         dases = []
         for arch_tag in ("amd64", "arm64", "riscv64"):
             try:
                 processor = getUtility(IProcessorSet).getByName(arch_tag)
             except ProcessorNotFound:
                 processor = self.factory.makeProcessor(
-                    name=arch_tag, supports_virtualized=True)
+                    name=arch_tag, supports_virtualized=True
+                )
             for distro_series in distro_serieses:
-                dases.append(self.factory.makeDistroArchSeries(
-                    distroseries=distro_series, architecturetag=arch_tag,
-                    processor=processor))
+                dases.append(
+                    self.factory.makeDistroArchSeries(
+                        distroseries=distro_series,
+                        architecturetag=arch_tag,
+                        processor=processor,
+                    )
+                )
         charmcraft_data = {}
         if self.bases is not None:
             charmcraft_data["bases"] = self.bases
         build_instances_factory = partial(
             determine_instances_to_build,
-            charmcraft_data, dases, distro_serieses[0])
+            charmcraft_data,
+            dases,
+            distro_serieses[0],
+        )
         if hasattr(self, "expected_exception"):
             self.assertThat(
-                build_instances_factory, Raises(self.expected_exception))
+                build_instances_factory, Raises(self.expected_exception)
+            )
         else:
-            self.assertThat(build_instances_factory(), MatchesListwise([
-                MatchesStructure(
-                    distroseries=MatchesStructure.byEquality(version=version),
-                    architecturetag=Equals(arch_tag))
-                for version, arch_tag in self.expected]))
+            self.assertThat(
+                build_instances_factory(),
+                MatchesListwise(
+                    [
+                        MatchesStructure(
+                            distroseries=MatchesStructure.byEquality(
+                                version=version
+                            ),
+                            architecturetag=Equals(arch_tag),
+                        )
+                        for version, arch_tag in self.expected
+                    ]
+                ),
+            )
 
 
 load_tests = load_tests_apply_scenarios
diff --git a/lib/lp/charms/browser/charmbase.py b/lib/lp/charms/browser/charmbase.py
index d882be6..6ab8fd3 100644
--- a/lib/lp/charms/browser/charmbase.py
+++ b/lib/lp/charms/browser/charmbase.py
@@ -5,7 +5,7 @@
 
 __all__ = [
     "CharmBaseSetNavigation",
-    ]
+]
 
 from zope.component import getUtility
 
@@ -15,6 +15,7 @@ from lp.services.webapp.publisher import Navigation
 
 class CharmBaseSetNavigation(Navigation):
     """Navigation methods for `ICharmBaseSet`."""
+
     usedfor = ICharmBaseSet
 
     def traverse(self, name):
diff --git a/lib/lp/charms/browser/charmrecipe.py b/lib/lp/charms/browser/charmrecipe.py
index 675e517..72321f9 100644
--- a/lib/lp/charms/browser/charmrecipe.py
+++ b/lib/lp/charms/browser/charmrecipe.py
@@ -15,45 +15,34 @@ __all__ = [
     "CharmRecipeRequestBuildsView",
     "CharmRecipeURL",
     "CharmRecipeView",
-    ]
+]
 
-from lazr.restful.interface import (
-    copy_field,
-    use_template,
-    )
+from lazr.restful.interface import copy_field, use_template
 from zope.component import getUtility
 from zope.error.interfaces import IErrorReportingUtility
-from zope.interface import (
-    implementer,
-    Interface,
-    )
-from zope.schema import (
-    Dict,
-    TextLine,
-    )
+from zope.interface import Interface, implementer
+from zope.schema import Dict, TextLine
 from zope.security.interfaces import Unauthorized
 
 from lp import _
 from lp.app.browser.launchpadform import (
-    action,
     LaunchpadEditFormView,
     LaunchpadFormView,
-    )
+    action,
+)
 from lp.app.browser.lazrjs import InlinePersonEditPickerWidget
 from lp.app.browser.tales import format_link
 from lp.charms.browser.widgets.charmrecipebuildchannels import (
     CharmRecipeBuildChannelsWidget,
-    )
-from lp.charms.interfaces.charmhubclient import (
-    BadRequestPackageUploadResponse,
-    )
+)
+from lp.charms.interfaces.charmhubclient import BadRequestPackageUploadResponse
 from lp.charms.interfaces.charmrecipe import (
-    CannotAuthorizeCharmhubUploads,
     CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG,
+    CannotAuthorizeCharmhubUploads,
     ICharmRecipe,
     ICharmRecipeSet,
     NoSuchCharmRecipe,
-    )
+)
 from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet
 from lp.code.browser.widgets.gitref import GitRefWidget
 from lp.code.interfaces.gitref import IGitRef
@@ -63,20 +52,17 @@ from lp.services.features import getFeatureFlag
 from lp.services.propertycache import cachedproperty
 from lp.services.utils import seconds_since_epoch
 from lp.services.webapp import (
-    canonical_url,
     ContextMenu,
-    enabled_with_permission,
     LaunchpadView,
     Link,
     Navigation,
     NavigationMenu,
+    canonical_url,
+    enabled_with_permission,
     stepthrough,
     structured,
-    )
-from lp.services.webapp.breadcrumb import (
-    Breadcrumb,
-    NameBreadcrumb,
-    )
+)
+from lp.services.webapp.breadcrumb import Breadcrumb, NameBreadcrumb
 from lp.services.webapp.candid import request_candid_discharge
 from lp.services.webapp.interfaces import ICanonicalUrlData
 from lp.services.webhooks.browser import WebhookTargetNavigationMixin
@@ -87,6 +73,7 @@ from lp.soyuz.browser.build import get_build_by_id_str
 @implementer(ICanonicalUrlData)
 class CharmRecipeURL:
     """Charm recipe URL creation rules."""
+
     rootsite = "mainsite"
 
     def __init__(self, recipe):
@@ -123,14 +110,15 @@ class CharmRecipeNavigation(WebhookTargetNavigationMixin, Navigation):
 
 
 class CharmRecipeBreadcrumb(NameBreadcrumb):
-
     @property
     def inside(self):
         # XXX cjwatson 2021-06-04: This should probably link to an
         # appropriate listing view, but we don't have one of those yet.
         return Breadcrumb(
-            self.context.project, text=self.context.project.display_name,
-            inside=self.context.project)
+            self.context.project,
+            text=self.context.project.display_name,
+            inside=self.context.project,
+        )
 
 
 class CharmRecipeNavigationMenu(NavigationMenu):
@@ -153,10 +141,14 @@ class CharmRecipeNavigationMenu(NavigationMenu):
     @enabled_with_permission("launchpad.Edit")
     def webhooks(self):
         return Link(
-            "+webhooks", "Manage webhooks", icon="edit",
+            "+webhooks",
+            "Manage webhooks",
+            icon="edit",
             enabled=(
-                bool(getFeatureFlag(CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG)) and
-                bool(getFeatureFlag("webhooks.new.enabled"))))
+                bool(getFeatureFlag(CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG))
+                and bool(getFeatureFlag("webhooks.new.enabled"))
+            ),
+        )
 
     @enabled_with_permission("launchpad.Edit")
     def authorize(self):
@@ -196,10 +188,15 @@ class CharmRecipeView(LaunchpadView):
     def person_picker(self):
         field = copy_field(
             ICharmRecipe["owner"],
-            vocabularyName="AllUserTeamsParticipationPlusSelfSimpleDisplay")
+            vocabularyName="AllUserTeamsParticipationPlusSelfSimpleDisplay",
+        )
         return InlinePersonEditPickerWidget(
-            self.context, field, format_link(self.context.owner),
-            header="Change owner", step_title="Select a new owner")
+            self.context,
+            field,
+            format_link(self.context.owner),
+            header="Change owner",
+            step_title="Select a new owner",
+        )
 
     @property
     def build_frequency(self):
@@ -253,34 +250,42 @@ def builds_and_requests_for_recipe(recipe):
 
     items = sorted(
         list(recipe.pending_builds) + list(recipe.pending_build_requests),
-        key=make_sort_key("date_created", "date_requested"))
+        key=make_sort_key("date_created", "date_requested"),
+    )
     if len(items) < 10:
         # We need to interleave two unbounded result sets, but we only need
         # enough items from them to make the total count up to 10.  It's
         # simplest to just fetch the upper bound from each set and do our
         # own sorting.
         recent_items = sorted(
-            list(recipe.completed_builds[:10 - len(items)]) +
-            list(recipe.failed_build_requests[:10 - len(items)]),
+            list(recipe.completed_builds[: 10 - len(items)])
+            + list(recipe.failed_build_requests[: 10 - len(items)]),
             key=make_sort_key(
-                "date_finished", "date_started",
-                "date_created", "date_requested"))
-        items.extend(recent_items[:10 - len(items)])
+                "date_finished",
+                "date_started",
+                "date_created",
+                "date_requested",
+            ),
+        )
+        items.extend(recent_items[: 10 - len(items)])
     return items
 
 
 class ICharmRecipeEditSchema(Interface):
     """Schema for adding or editing a charm recipe."""
 
-    use_template(ICharmRecipe, include=[
-        "owner",
-        "name",
-        "project",
-        "require_virtualized",
-        "auto_build",
-        "auto_build_channels",
-        "store_upload",
-        ])
+    use_template(
+        ICharmRecipe,
+        include=[
+            "owner",
+            "name",
+            "project",
+            "require_virtualized",
+            "auto_build",
+            "auto_build_channels",
+            "store_upload",
+        ],
+    )
 
     git_ref = copy_field(ICharmRecipe["git_ref"], required=True)
 
@@ -297,15 +302,16 @@ def log_oops(error, request):
 
 
 class CharmRecipeAuthorizeMixin:
-
     def requestAuthorization(self, recipe):
         try:
             self.next_url = CharmRecipeAuthorizeView.requestAuthorization(
-                recipe, self.request)
+                recipe, self.request
+            )
         except BadRequestPackageUploadResponse as e:
             self.setFieldError(
                 "store_upload",
-                "Cannot get permission from Charmhub to upload this package.")
+                "Cannot get permission from Charmhub to upload this package.",
+            )
             log_oops(e, self.request)
 
 
@@ -333,7 +339,7 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
             "store_upload",
             "store_name",
             "store_channels",
-            ]
+        ]
 
     @property
     def is_project_context(self):
@@ -346,8 +352,9 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
     @property
     def initial_values(self):
         initial_values = {"owner": self.user}
-        if (IGitRef.providedBy(self.context) and
-                IProduct.providedBy(self.context.target)):
+        if IGitRef.providedBy(self.context) and IProduct.providedBy(
+            self.context.target
+        ):
             initial_values["project"] = self.context.target
         return initial_values
 
@@ -372,14 +379,20 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
             git_ref = data["git_ref"]
         else:
             raise NotImplementedError(
-                "Unknown context for charm recipe creation.")
+                "Unknown context for charm recipe creation."
+            )
         recipe = getUtility(ICharmRecipeSet).new(
-            self.user, data["owner"], project, data["name"], git_ref=git_ref,
+            self.user,
+            data["owner"],
+            project,
+            data["name"],
+            git_ref=git_ref,
             auto_build=data["auto_build"],
             auto_build_channels=data["auto_build_channels"],
             store_upload=data["store_upload"],
             store_name=data["store_name"],
-            store_channels=data.get("store_channels"))
+            store_channels=data.get("store_channels"),
+        )
         if data["store_upload"]:
             self.requestAuthorization(recipe)
         else:
@@ -398,11 +411,13 @@ class CharmRecipeAddView(CharmRecipeAuthorizeMixin, LaunchpadFormView):
                 self.setFieldError(
                     "name",
                     "There is already a charm recipe owned by %s in %s with "
-                    "this name." % (owner.display_name, project.display_name))
+                    "this name." % (owner.display_name, project.display_name),
+                )
 
 
 class BaseCharmRecipeEditView(
-        CharmRecipeAuthorizeMixin, LaunchpadEditFormView):
+    CharmRecipeAuthorizeMixin, LaunchpadEditFormView
+):
 
     schema = ICharmRecipeEditSchema
 
@@ -429,13 +444,15 @@ class BaseCharmRecipeEditView(
             if owner is not None and owner.private:
                 self.setFieldError(
                     "owner",
-                    "A public charm recipe cannot have a private owner.")
+                    "A public charm recipe cannot have a private owner.",
+                )
         if "git_ref" in data:
             ref = data.get("git_ref", self.context.git_ref)
             if ref is not None and ref.private:
                 self.setFieldError(
                     "git_ref",
-                    "A public charm recipe cannot have a private repository.")
+                    "A public charm recipe cannot have a private repository.",
+                )
 
     def _needCharmhubReauth(self, data):
         """Does this change require reauthorizing to Charmhub?"""
@@ -444,8 +461,9 @@ class BaseCharmRecipeEditView(
         if not store_upload or store_name is None:
             return False
         return (
-            not self.context.store_upload or
-            store_name != self.context.store_name)
+            not self.context.store_upload
+            or store_name != self.context.store_name
+        )
 
     @action("Update charm recipe", name="update")
     def request_action(self, action, data):
@@ -502,7 +520,7 @@ class CharmRecipeEditView(BaseCharmRecipeEditView):
         "store_upload",
         "store_name",
         "store_channels",
-        ]
+    ]
     custom_widget_git_ref = GitRefWidget
     custom_widget_auto_build_channels = CharmRecipeBuildChannelsWidget
     custom_widget_store_channels = StoreChannelsWidget
@@ -515,13 +533,15 @@ class CharmRecipeEditView(BaseCharmRecipeEditView):
         if owner and project and name:
             try:
                 recipe = getUtility(ICharmRecipeSet).getByName(
-                    owner, project, name)
+                    owner, project, name
+                )
                 if recipe != self.context:
                     self.setFieldError(
                         "name",
                         "There is already a charm recipe owned by %s in %s "
-                        "with this name." %
-                        (owner.display_name, project.display_name))
+                        "with this name."
+                        % (owner.display_name, project.display_name),
+                    )
             except NoSuchCharmRecipe:
                 pass
 
@@ -539,7 +559,8 @@ class CharmRecipeAuthorizeView(LaunchpadEditFormView):
         """Schema for authorizing charm recipe uploads to Charmhub."""
 
         discharge_macaroon = TextLine(
-            title="Serialized discharge macaroon", required=True)
+            title="Serialized discharge macaroon", required=True
+        )
 
     render_context = False
 
@@ -560,9 +581,12 @@ class CharmRecipeAuthorizeView(LaunchpadEditFormView):
         else:
             base_url = canonical_url(recipe, view_name="+authorize")
             return request_candid_discharge(
-                request, root_macaroon_raw, base_url,
+                request,
+                root_macaroon_raw,
+                base_url,
                 "field.discharge_macaroon",
-                discharge_macaroon_action="field.actions.complete")
+                discharge_macaroon_action="field.actions.complete",
+            )
 
     @action("Begin authorization", name="begin")
     def begin_action(self, action, data):
@@ -573,14 +597,23 @@ class CharmRecipeAuthorizeView(LaunchpadEditFormView):
     @action("Complete authorization", name="complete")
     def complete_action(self, action, data):
         if not data.get("discharge_macaroon"):
-            self.addError(structured(
-                _("Uploads of %(recipe)s to Charmhub were not authorized."),
-                recipe=self.context.name))
+            self.addError(
+                structured(
+                    _(
+                        "Uploads of %(recipe)s to Charmhub were not "
+                        "authorized."
+                    ),
+                    recipe=self.context.name,
+                )
+            )
             return
         self.context.completeAuthorization(data["discharge_macaroon"])
-        self.request.response.addInfoNotification(structured(
-            _("Uploads of %(recipe)s to Charmhub are now authorized."),
-            recipe=self.context.name))
+        self.request.response.addInfoNotification(
+            structured(
+                _("Uploads of %(recipe)s to Charmhub are now authorized."),
+                recipe=self.context.name,
+            )
+        )
         self.request.response.redirect(canonical_url(self.context))
 
     @property
@@ -620,8 +653,11 @@ class CharmRecipeRequestBuildsView(LaunchpadFormView):
         """Schema for requesting a build."""
 
         channels = Dict(
-            title="Source snap channels", key_type=TextLine(), required=True,
-            description=ICharmRecipe["auto_build_channels"].description)
+            title="Source snap channels",
+            key_type=TextLine(),
+            required=True,
+            description=ICharmRecipe["auto_build_channels"].description,
+        )
 
     custom_widget_channels = CharmRecipeBuildChannelsWidget
 
@@ -634,11 +670,12 @@ class CharmRecipeRequestBuildsView(LaunchpadFormView):
         """See `LaunchpadFormView`."""
         return {
             "channels": self.context.auto_build_channels,
-            }
+        }
 
     @action("Request builds", name="request")
     def request_action(self, action, data):
         self.context.requestBuilds(self.user, channels=data["channels"])
         self.request.response.addNotification(
-            _("Builds will be dispatched soon."))
+            _("Builds will be dispatched soon.")
+        )
         self.next_url = self.cancel_url
diff --git a/lib/lp/charms/browser/charmrecipebuild.py b/lib/lp/charms/browser/charmrecipebuild.py
index 4c6ae4e..0b90fcf 100644
--- a/lib/lp/charms/browser/charmrecipebuild.py
+++ b/lib/lp/charms/browser/charmrecipebuild.py
@@ -7,30 +7,27 @@ __all__ = [
     "CharmRecipeBuildContextMenu",
     "CharmRecipeBuildNavigation",
     "CharmRecipeBuildView",
-    ]
+]
 
 from zope.interface import Interface
 
-from lp.app.browser.launchpadform import (
-    action,
-    LaunchpadFormView,
-    )
+from lp.app.browser.launchpadform import LaunchpadFormView, action
 from lp.charms.interfaces.charmrecipebuild import (
     CannotScheduleStoreUpload,
     ICharmRecipeBuild,
-    )
+)
 from lp.services.librarian.browser import (
     FileNavigationMixin,
     ProxiedLibraryFileAlias,
-    )
+)
 from lp.services.propertycache import cachedproperty
 from lp.services.webapp import (
-    canonical_url,
     ContextMenu,
-    enabled_with_permission,
     Link,
     Navigation,
-    )
+    canonical_url,
+    enabled_with_permission,
+)
 from lp.soyuz.interfaces.binarypackagebuild import IBuildRescoreForm
 
 
@@ -50,20 +47,29 @@ class CharmRecipeBuildContextMenu(ContextMenu):
     @enabled_with_permission("launchpad.Edit")
     def retry(self):
         return Link(
-            "+retry", "Retry this build", icon="retry",
-            enabled=self.context.can_be_retried)
+            "+retry",
+            "Retry this build",
+            icon="retry",
+            enabled=self.context.can_be_retried,
+        )
 
     @enabled_with_permission("launchpad.Edit")
     def cancel(self):
         return Link(
-            "+cancel", "Cancel build", icon="remove",
-            enabled=self.context.can_be_cancelled)
+            "+cancel",
+            "Cancel build",
+            icon="remove",
+            enabled=self.context.can_be_cancelled,
+        )
 
     @enabled_with_permission("launchpad.Admin")
     def rescore(self):
         return Link(
-            "+rescore", "Rescore build", icon="edit",
-            enabled=self.context.can_be_rescored)
+            "+rescore",
+            "Rescore build",
+            icon="edit",
+            enabled=self.context.can_be_rescored,
+        )
 
 
 class CharmRecipeBuildView(LaunchpadFormView):
@@ -86,7 +92,9 @@ class CharmRecipeBuildView(LaunchpadFormView):
 
         return [
             ProxiedLibraryFileAlias(alias, self.context)
-            for _, alias, _ in self.context.getFiles() if not alias.deleted]
+            for _, alias, _ in self.context.getFiles()
+            if not alias.deleted
+        ]
 
     @cachedproperty
     def has_files(self):
@@ -106,7 +114,8 @@ class CharmRecipeBuildView(LaunchpadFormView):
         else:
             self.request.response.addInfoNotification(
                 "An upload has been scheduled and will run as soon as "
-                "possible.")
+                "possible."
+            )
 
 
 class CharmRecipeBuildRetryView(LaunchpadFormView):
@@ -120,6 +129,7 @@ class CharmRecipeBuildRetryView(LaunchpadFormView):
     @property
     def cancel_url(self):
         return canonical_url(self.context)
+
     next_url = cancel_url
 
     @action("Retry build", name="retry")
@@ -127,7 +137,8 @@ class CharmRecipeBuildRetryView(LaunchpadFormView):
         """Retry the build."""
         if not self.context.can_be_retried:
             self.request.response.addErrorNotification(
-                "Build cannot be retried")
+                "Build cannot be retried"
+            )
         else:
             self.context.retry()
             self.request.response.addInfoNotification("Build has been queued")
@@ -146,6 +157,7 @@ class CharmRecipeBuildCancelView(LaunchpadFormView):
     @property
     def cancel_url(self):
         return canonical_url(self.context)
+
     next_url = cancel_url
 
     @action("Cancel build", name="cancel")
@@ -165,12 +177,14 @@ class CharmRecipeBuildRescoreView(LaunchpadFormView):
         if self.context.can_be_rescored:
             return super().__call__()
         self.request.response.addWarningNotification(
-            "Cannot rescore this build because it is not queued.")
+            "Cannot rescore this build because it is not queued."
+        )
         self.request.response.redirect(canonical_url(self.context))
 
     @property
     def cancel_url(self):
         return canonical_url(self.context)
+
     next_url = cancel_url
 
     @action("Rescore build", name="rescore")
diff --git a/lib/lp/charms/browser/charmrecipelisting.py b/lib/lp/charms/browser/charmrecipelisting.py
index 90fa225..4a47a81 100644
--- a/lib/lp/charms/browser/charmrecipelisting.py
+++ b/lib/lp/charms/browser/charmrecipelisting.py
@@ -7,7 +7,7 @@ __all__ = [
     "GitCharmRecipeListingView",
     "PersonCharmRecipeListingView",
     "ProjectCharmRecipeListingView",
-    ]
+]
 
 from functools import partial
 
@@ -35,14 +35,17 @@ class CharmRecipeListingView(LaunchpadView, FeedsMixin):
     @property
     def label(self):
         return "Charm recipes for %(displayname)s" % {
-            "displayname": self.context.displayname}
+            "displayname": self.context.displayname
+        }
 
     def initialize(self):
         super().initialize()
         recipes = getUtility(ICharmRecipeSet).findByContext(
-            self.context, visible_by_user=self.user)
+            self.context, visible_by_user=self.user
+        )
         loader = partial(
-            getUtility(ICharmRecipeSet).preloadDataForRecipes, user=self.user)
+            getUtility(ICharmRecipeSet).preloadDataForRecipes, user=self.user
+        )
         self.recipes = DecoratedResultSet(recipes, pre_iter_hook=loader)
 
     @cachedproperty
@@ -57,7 +60,8 @@ class GitCharmRecipeListingView(CharmRecipeListingView):
     @property
     def label(self):
         return "Charm recipes for %(display_name)s" % {
-            "display_name": self.context.display_name}
+            "display_name": self.context.display_name
+        }
 
 
 class PersonCharmRecipeListingView(CharmRecipeListingView):
diff --git a/lib/lp/charms/browser/hascharmrecipes.py b/lib/lp/charms/browser/hascharmrecipes.py
index b73f866..854f269 100644
--- a/lib/lp/charms/browser/hascharmrecipes.py
+++ b/lib/lp/charms/browser/hascharmrecipes.py
@@ -6,7 +6,7 @@
 __all__ = [
     "HasCharmRecipesMenuMixin",
     "HasCharmRecipesViewMixin",
-    ]
+]
 
 from zope.component import getUtility
 
@@ -14,13 +14,10 @@ from lp.charms.interfaces.charmrecipe import (
     CHARM_RECIPE_ALLOW_CREATE,
     CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
     ICharmRecipeSet,
-    )
+)
 from lp.code.interfaces.gitrepository import IGitRepository
 from lp.services.features import getFeatureFlag
-from lp.services.webapp import (
-    canonical_url,
-    Link,
-    )
+from lp.services.webapp import Link, canonical_url
 from lp.services.webapp.escaping import structured
 
 
@@ -29,17 +26,20 @@ class HasCharmRecipesMenuMixin:
 
     def view_charm_recipes(self):
         text = "View charm recipes"
-        enabled = not getUtility(ICharmRecipeSet).findByContext(
-            self.context, visible_by_user=self.user).is_empty()
+        enabled = (
+            not getUtility(ICharmRecipeSet)
+            .findByContext(self.context, visible_by_user=self.user)
+            .is_empty()
+        )
         return Link("+charm-recipes", text, icon="info", enabled=enabled)
 
     def create_charm_recipe(self):
         # Only enabled for private contexts if the
         # charm.recipe.allow_private flag is enabled.
-        enabled = (
-            bool(getFeatureFlag(CHARM_RECIPE_ALLOW_CREATE)) and (
-                not self.context.private or
-                bool(getFeatureFlag(CHARM_RECIPE_PRIVATE_FEATURE_FLAG))))
+        enabled = bool(getFeatureFlag(CHARM_RECIPE_ALLOW_CREATE)) and (
+            not self.context.private
+            or bool(getFeatureFlag(CHARM_RECIPE_PRIVATE_FEATURE_FLAG))
+        )
 
         text = "Create charm recipe"
         return Link("+new-charm-recipe", text, enabled=enabled, icon="add")
@@ -51,7 +51,8 @@ class HasCharmRecipesViewMixin:
     @property
     def charm_recipes(self):
         return getUtility(ICharmRecipeSet).findByContext(
-            self.context, visible_by_user=self.user)
+            self.context, visible_by_user=self.user
+        )
 
     @property
     def charm_recipes_link(self):
@@ -69,9 +70,12 @@ class HasCharmRecipesViewMixin:
             return structured(
                 '<a href="%s">1 charm recipe</a> using this %s.',
                 canonical_url(self.charm_recipes.one()),
-                context_type).escapedtext
+                context_type,
+            ).escapedtext
         else:
             # Link to a charm recipe listing.
             return structured(
                 '<a href="+charm-recipes">%s charm recipes</a> using this %s.',
-                count, context_type).escapedtext
+                count,
+                context_type,
+            ).escapedtext
diff --git a/lib/lp/charms/browser/tests/test_charmrecipe.py b/lib/lp/charms/browser/tests/test_charmrecipe.py
index 2324fa6..0a0a377 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipe.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipe.py
@@ -4,24 +4,19 @@
 """Test charm recipe views."""
 
 import base64
-from datetime import (
-    datetime,
-    timedelta,
-    )
 import json
 import re
-from urllib.parse import (
-    parse_qs,
-    urlsplit,
-    )
+from datetime import datetime, timedelta
+from urllib.parse import parse_qs, urlsplit
 
+import pytz
+import responses
+import soupmatchers
+import transaction
 from fixtures import FakeLogger
 from nacl.public import PrivateKey
 from pymacaroons import Macaroon
 from pymacaroons.serializers import JsonSerializer
-import pytz
-import responses
-import soupmatchers
 from testtools.matchers import (
     AfterPreprocessing,
     ContainsDict,
@@ -30,8 +25,7 @@ from testtools.matchers import (
     MatchesDict,
     MatchesListwise,
     MatchesStructure,
-    )
-import transaction
+)
 from zope.component import getUtility
 from zope.publisher.interfaces import NotFound
 from zope.security.interfaces import Unauthorized
@@ -46,12 +40,12 @@ from lp.charms.browser.charmrecipe import (
     CharmRecipeAdminView,
     CharmRecipeEditView,
     CharmRecipeView,
-    )
+)
 from lp.charms.interfaces.charmrecipe import (
     CHARM_RECIPE_ALLOW_CREATE,
     CharmRecipeBuildRequestStatus,
     ICharmRecipeSet,
-    )
+)
 from lp.registry.enums import PersonVisibility
 from lp.services.crypto.interfaces import IEncryptedContainer
 from lp.services.database.constants import UTC_NOW
@@ -62,33 +56,24 @@ from lp.services.webapp import canonical_url
 from lp.services.webapp.servers import LaunchpadTestRequest
 from lp.testing import (
     BrowserTestCase,
+    TestCaseWithFactory,
     login,
     login_admin,
     login_person,
     person_logged_in,
-    TestCaseWithFactory,
     time_counter,
-    )
-from lp.testing.layers import (
-    DatabaseFunctionalLayer,
-    LaunchpadFunctionalLayer,
-    )
-from lp.testing.matchers import (
-    MatchesPickerText,
-    MatchesTagText,
-    )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
+from lp.testing.matchers import MatchesPickerText, MatchesTagText
 from lp.testing.pages import (
     extract_text,
     find_main_content,
     find_tag_by_id,
     find_tags_by_class,
     get_feedback_messages,
-    )
+)
 from lp.testing.publication import test_traverse
-from lp.testing.views import (
-    create_initialized_view,
-    create_view,
-    )
+from lp.testing.views import create_initialized_view, create_view
 
 
 class TestCharmRecipeNavigation(TestCaseWithFactory):
@@ -103,16 +88,19 @@ class TestCharmRecipeNavigation(TestCaseWithFactory):
         owner = self.factory.makePerson(name="person")
         project = self.factory.makeProduct(name="project")
         recipe = self.factory.makeCharmRecipe(
-            registrant=owner, owner=owner, project=project, name="charm")
+            registrant=owner, owner=owner, project=project, name="charm"
+        )
         self.assertEqual(
             "http://launchpad.test/~person/project/+charm/charm";,
-            canonical_url(recipe))
+            canonical_url(recipe),
+        )
 
     def test_charm_recipe(self):
         recipe = self.factory.makeCharmRecipe()
         obj, _, _ = test_traverse(
-            "http://launchpad.test/~%s/%s/+charm/%s"; % (
-                recipe.owner.name, recipe.project.name, recipe.name))
+            "http://launchpad.test/~%s/%s/+charm/%s";
+            % (recipe.owner.name, recipe.project.name, recipe.name)
+        )
         self.assertEqual(recipe, obj)
 
 
@@ -125,25 +113,32 @@ class BaseTestCharmRecipeView(BrowserTestCase):
         self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
         self.useFixture(FakeLogger())
         self.person = self.factory.makePerson(
-            name="test-person", displayname="Test Person")
+            name="test-person", displayname="Test Person"
+        )
 
 
 class TestCharmRecipeAddView(BaseTestCharmRecipeView):
-
     def test_create_new_recipe_not_logged_in(self):
         [git_ref] = self.factory.makeGitRefs()
         self.assertRaises(
-            Unauthorized, self.getViewBrowser, git_ref,
-            view_name="+new-charm-recipe", no_login=True)
+            Unauthorized,
+            self.getViewBrowser,
+            git_ref,
+            view_name="+new-charm-recipe",
+            no_login=True,
+        )
 
     def test_create_new_recipe_git(self):
         self.factory.makeProduct(
-            name="test-project", displayname="Test Project")
+            name="test-project", displayname="Test Project"
+        )
         [git_ref] = self.factory.makeGitRefs(
-            owner=self.person, target=self.person)
+            owner=self.person, target=self.person
+        )
         source_display = git_ref.display_name
         browser = self.getViewBrowser(
-            git_ref, view_name="+new-charm-recipe", user=self.person)
+            git_ref, view_name="+new-charm-recipe", user=self.person
+        )
         browser.getControl(name="field.name").value = "charm-name"
         self.assertEqual("", browser.getControl(name="field.project").value)
         browser.getControl(name="field.project").value = "test-project"
@@ -152,129 +147,161 @@ class TestCharmRecipeAddView(BaseTestCharmRecipeView):
         content = find_main_content(browser.contents)
         self.assertEqual("charm-name", extract_text(content.h1))
         self.assertThat(
-            "Test Person", MatchesPickerText(content, "edit-owner"))
+            "Test Person", MatchesPickerText(content, "edit-owner")
+        )
         self.assertThat(
             "Project:\nTest Project\nEdit charm recipe",
-            MatchesTagText(content, "project"))
+            MatchesTagText(content, "project"),
+        )
         self.assertThat(
             "Source:\n%s\nEdit charm recipe" % source_display,
-            MatchesTagText(content, "source"))
+            MatchesTagText(content, "source"),
+        )
         self.assertThat(
             "Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n",
-            MatchesTagText(content, "auto_build"))
+            MatchesTagText(content, "auto_build"),
+        )
         self.assertIsNone(find_tag_by_id(content, "auto_build_channels"))
         self.assertThat(
             "Builds of this charm recipe are not automatically uploaded to "
             "the store.\nEdit charm recipe",
-            MatchesTagText(content, "store_upload"))
+            MatchesTagText(content, "store_upload"),
+        )
 
     def test_create_new_recipe_git_project_namespace(self):
         # If the Git repository is already in a project namespace, then that
         # project is the default for the new recipe.
         project = self.factory.makeProduct(
-            name="test-project", displayname="Test Project")
+            name="test-project", displayname="Test Project"
+        )
         [git_ref] = self.factory.makeGitRefs(target=project)
         source_display = git_ref.display_name
         browser = self.getViewBrowser(
-            git_ref, view_name="+new-charm-recipe", user=self.person)
+            git_ref, view_name="+new-charm-recipe", user=self.person
+        )
         browser.getControl(name="field.name").value = "charm-name"
         self.assertEqual(
-            "test-project", browser.getControl(name="field.project").value)
+            "test-project", browser.getControl(name="field.project").value
+        )
         browser.getControl("Create charm recipe").click()
 
         content = find_main_content(browser.contents)
         self.assertEqual("charm-name", extract_text(content.h1))
         self.assertThat(
-            "Test Person", MatchesPickerText(content, "edit-owner"))
+            "Test Person", MatchesPickerText(content, "edit-owner")
+        )
         self.assertThat(
             "Project:\nTest Project\nEdit charm recipe",
-            MatchesTagText(content, "project"))
+            MatchesTagText(content, "project"),
+        )
         self.assertThat(
             "Source:\n%s\nEdit charm recipe" % source_display,
-            MatchesTagText(content, "source"))
+            MatchesTagText(content, "source"),
+        )
         self.assertThat(
             "Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n",
-            MatchesTagText(content, "auto_build"))
+            MatchesTagText(content, "auto_build"),
+        )
         self.assertIsNone(find_tag_by_id(content, "auto_build_channels"))
         self.assertThat(
             "Builds of this charm recipe are not automatically uploaded to "
             "the store.\nEdit charm recipe",
-            MatchesTagText(content, "store_upload"))
+            MatchesTagText(content, "store_upload"),
+        )
 
     def test_create_new_recipe_project(self):
         project = self.factory.makeProduct(displayname="Test Project")
         [git_ref] = self.factory.makeGitRefs()
         source_display = git_ref.display_name
         browser = self.getViewBrowser(
-            project, view_name="+new-charm-recipe", user=self.person)
+            project, view_name="+new-charm-recipe", user=self.person
+        )
         browser.getControl(name="field.name").value = "charm-name"
-        browser.getControl(name="field.git_ref.repository").value = (
-            git_ref.repository.shortened_path)
+        browser.getControl(
+            name="field.git_ref.repository"
+        ).value = git_ref.repository.shortened_path
         browser.getControl(name="field.git_ref.path").value = git_ref.path
         browser.getControl("Create charm recipe").click()
 
         content = find_main_content(browser.contents)
         self.assertEqual("charm-name", extract_text(content.h1))
         self.assertThat(
-            "Test Person", MatchesPickerText(content, "edit-owner"))
+            "Test Person", MatchesPickerText(content, "edit-owner")
+        )
         self.assertThat(
             "Project:\nTest Project\nEdit charm recipe",
-            MatchesTagText(content, "project"))
+            MatchesTagText(content, "project"),
+        )
         self.assertThat(
             "Source:\n%s\nEdit charm recipe" % source_display,
-            MatchesTagText(content, "source"))
+            MatchesTagText(content, "source"),
+        )
         self.assertThat(
             "Build schedule:\n(?)\nBuilt on request\nEdit charm recipe\n",
-            MatchesTagText(content, "auto_build"))
+            MatchesTagText(content, "auto_build"),
+        )
         self.assertIsNone(find_tag_by_id(content, "auto_build_channels"))
         self.assertThat(
             "Builds of this charm recipe are not automatically uploaded to "
             "the store.\nEdit charm recipe",
-            MatchesTagText(content, "store_upload"))
+            MatchesTagText(content, "store_upload"),
+        )
 
     def test_create_new_recipe_users_teams_as_owner_options(self):
         # Teams that the user is in are options for the charm recipe owner.
         self.factory.makeTeam(
-            name="test-team", displayname="Test Team", members=[self.person])
+            name="test-team", displayname="Test Team", members=[self.person]
+        )
         [git_ref] = self.factory.makeGitRefs()
         browser = self.getViewBrowser(
-            git_ref, view_name="+new-charm-recipe", user=self.person)
+            git_ref, view_name="+new-charm-recipe", user=self.person
+        )
         options = browser.getControl("Owner").displayOptions
         self.assertEqual(
             ["Test Person (test-person)", "Test Team (test-team)"],
-            sorted(str(option) for option in options))
+            sorted(str(option) for option in options),
+        )
 
     def test_create_new_recipe_auto_build(self):
         # Creating a new recipe and asking for it to be automatically built
         # sets all the appropriate fields.
         self.factory.makeProduct(
-            name="test-project", displayname="Test Project")
+            name="test-project", displayname="Test Project"
+        )
         [git_ref] = self.factory.makeGitRefs()
         browser = self.getViewBrowser(
-            git_ref, view_name="+new-charm-recipe", user=self.person)
+            git_ref, view_name="+new-charm-recipe", user=self.person
+        )
         browser.getControl(name="field.name").value = "charm-name"
         browser.getControl(name="field.project").value = "test-project"
         browser.getControl(
-            "Automatically build when branch changes").selected = True
+            "Automatically build when branch changes"
+        ).selected = True
         browser.getControl(
-            name="field.auto_build_channels.charmcraft").value = "edge"
+            name="field.auto_build_channels.charmcraft"
+        ).value = "edge"
         browser.getControl(
-            name="field.auto_build_channels.core").value = "stable"
+            name="field.auto_build_channels.core"
+        ).value = "stable"
         browser.getControl(
-            name="field.auto_build_channels.core18").value = "beta"
+            name="field.auto_build_channels.core18"
+        ).value = "beta"
         browser.getControl(
-            name="field.auto_build_channels.core20").value = "edge/feature"
+            name="field.auto_build_channels.core20"
+        ).value = "edge/feature"
         browser.getControl("Create charm recipe").click()
 
         content = find_main_content(browser.contents)
         self.assertThat(
             "Build schedule:\n(?)\nBuilt automatically\nEdit charm recipe\n",
-            MatchesTagText(content, "auto_build"))
+            MatchesTagText(content, "auto_build"),
+        )
         self.assertThat(
             "Source snap channels for automatic builds:\nEdit charm recipe\n"
             "charmcraft\nedge\ncore\nstable\ncore18\nbeta\n"
             "core20\nedge/feature\n",
-            MatchesTagText(content, "auto_build_channels"))
+            MatchesTagText(content, "auto_build_channels"),
+        )
 
     @responses.activate
     def test_create_new_recipe_store_upload(self):
@@ -285,9 +312,11 @@ class TestCharmRecipeAddView(BaseTestCharmRecipeView):
         self.pushConfig(
             "launchpad",
             candid_service_root="https://candid.test/";,
-            csrf_secret="test secret")
+            csrf_secret="test secret",
+        )
         project = self.factory.makeProduct(
-            name="test-project", displayname="Test Project")
+            name="test-project", displayname="Test Project"
+        )
         [git_ref] = self.factory.makeGitRefs()
         view_url = canonical_url(git_ref, view_name="+new-charm-recipe")
         browser = self.getNonRedirectingBrowser(url=view_url, user=self.person)
@@ -300,95 +329,147 @@ class TestCharmRecipeAddView(BaseTestCharmRecipeView):
         browser.getControl("Edge").selected = True
         root_macaroon = Macaroon(version=2)
         root_macaroon.add_third_party_caveat(
-            "https://candid.test/";, "", "identity")
+            "https://candid.test/";, "", "identity"
+        )
         caveat = root_macaroon.caveats[0]
         root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens";,
-            json={"macaroon": root_macaroon_raw})
+            "POST",
+            "http://charmhub.example/v1/tokens";,
+            json={"macaroon": root_macaroon_raw},
+        )
         responses.add(
-            "POST", "https://candid.test/discharge";, status=401,
+            "POST",
+            "https://candid.test/discharge";,
+            status=401,
             json={
                 "Code": "interaction required",
                 "Message": (
-                    "macaroon discharge required: authentication required"),
+                    "macaroon discharge required: authentication required"
+                ),
                 "Info": {
                     "InteractionMethods": {
                         "browser-redirect": {
                             "LoginURL": "https://candid.test/login-redirect";,
-                            },
                         },
                     },
-                })
+                },
+            },
+        )
         browser.getControl("Create charm recipe").click()
         login_person(self.person)
         recipe = getUtility(ICharmRecipeSet).getByName(
-            self.person, project, "charm-name")
-        self.assertThat(recipe, MatchesStructure.byEquality(
-            owner=self.person, project=project, name="charm-name",
-            source=git_ref, store_upload=True, store_name="charmhub-name",
-            store_secrets={"root": root_macaroon_raw},
-            store_channels=["track/edge"]))
-        self.assertThat(responses.calls, MatchesListwise([
-            MatchesStructure(
-                request=MatchesStructure(
-                    url=Equals("http://charmhub.example/v1/tokens";),
-                    method=Equals("POST"),
-                    body=AfterPreprocessing(
-                        lambda b: json.loads(b.decode()),
-                        Equals({
-                            "description": "charmhub-name for launchpad.test",
-                            "packages": [
-                                {"type": "charm", "name": "charmhub-name"},
-                                ],
-                            "permissions": [
-                                "package-manage-releases",
-                                "package-manage-revisions",
-                                "package-view-revisions",
-                                ],
-                            })))),
-            MatchesStructure(
-                request=MatchesStructure(
-                    url=Equals("https://candid.test/discharge";),
-                    headers=ContainsDict({
-                        "Content-Type": Equals(
-                            "application/x-www-form-urlencoded"),
-                        }),
-                    body=AfterPreprocessing(parse_qs, MatchesDict({
+            self.person, project, "charm-name"
+        )
+        self.assertThat(
+            recipe,
+            MatchesStructure.byEquality(
+                owner=self.person,
+                project=project,
+                name="charm-name",
+                source=git_ref,
+                store_upload=True,
+                store_name="charmhub-name",
+                store_secrets={"root": root_macaroon_raw},
+                store_channels=["track/edge"],
+            ),
+        )
+        tokens_matcher = MatchesStructure(
+            url=Equals("http://charmhub.example/v1/tokens";),
+            method=Equals("POST"),
+            body=AfterPreprocessing(
+                lambda b: json.loads(b.decode()),
+                Equals(
+                    {
+                        "description": ("charmhub-name for launchpad.test"),
+                        "packages": [
+                            {
+                                "type": "charm",
+                                "name": "charmhub-name",
+                            },
+                        ],
+                        "permissions": [
+                            "package-manage-releases",
+                            "package-manage-revisions",
+                            "package-view-revisions",
+                        ],
+                    }
+                ),
+            ),
+        )
+        discharge_matcher = MatchesStructure(
+            url=Equals("https://candid.test/discharge";),
+            headers=ContainsDict(
+                {
+                    "Content-Type": Equals(
+                        "application/x-www-form-urlencoded"
+                    ),
+                }
+            ),
+            body=AfterPreprocessing(
+                parse_qs,
+                MatchesDict(
+                    {
                         "id64": Equals(
-                            [base64.b64encode(
-                                caveat.caveat_id_bytes).decode()]),
-                        })))),
-            ]))
+                            [base64.b64encode(caveat.caveat_id_bytes).decode()]
+                        ),
+                    }
+                ),
+            ),
+        )
+        self.assertThat(
+            responses.calls,
+            MatchesListwise(
+                [
+                    MatchesStructure(request=tokens_matcher),
+                    MatchesStructure(request=discharge_matcher),
+                ]
+            ),
+        )
         self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
+        return_to_matcher = AfterPreprocessing(
+            urlsplit,
+            MatchesStructure(
+                scheme=Equals("http"),
+                netloc=Equals("launchpad.test"),
+                path=Equals("/+candid-callback"),
+                query=AfterPreprocessing(
+                    parse_qs,
+                    MatchesDict(
+                        {
+                            "starting_url": Equals(
+                                [canonical_url(recipe) + "/+authorize"]
+                            ),
+                            "discharge_macaroon_action": Equals(
+                                ["field.actions.complete"]
+                            ),
+                            "discharge_macaroon_field": Equals(
+                                ["field.discharge_macaroon"]
+                            ),
+                        }
+                    ),
+                ),
+                fragment=Equals(""),
+            ),
+        )
         self.assertThat(
             urlsplit(browser.headers["Location"]),
             MatchesStructure(
                 scheme=Equals("https"),
                 netloc=Equals("candid.test"),
                 path=Equals("/login-redirect"),
-                query=AfterPreprocessing(parse_qs, ContainsDict({
-                    "return_to": MatchesListwise([
-                        AfterPreprocessing(urlsplit, MatchesStructure(
-                            scheme=Equals("http"),
-                            netloc=Equals("launchpad.test"),
-                            path=Equals("/+candid-callback"),
-                            query=AfterPreprocessing(parse_qs, MatchesDict({
-                                "starting_url": Equals(
-                                    [canonical_url(recipe) + "/+authorize"]),
-                                "discharge_macaroon_action": Equals(
-                                    ["field.actions.complete"]),
-                                "discharge_macaroon_field": Equals(
-                                    ["field.discharge_macaroon"]),
-                                })),
-                            fragment=Equals(""))),
-                        ]),
-                    })),
-                fragment=Equals("")))
+                query=AfterPreprocessing(
+                    parse_qs,
+                    ContainsDict(
+                        {"return_to": MatchesListwise([return_to_matcher])}
+                    ),
+                ),
+                fragment=Equals(""),
+            ),
+        )
 
 
 class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
-
     def test_unauthorized(self):
         # A non-admin user cannot administer a charm recipe.
         login_person(self.person)
@@ -396,16 +477,21 @@ class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
         recipe_url = canonical_url(recipe)
         browser = self.getViewBrowser(recipe, user=self.person)
         self.assertRaises(
-            LinkNotFoundError, browser.getLink, "Administer charm recipe")
+            LinkNotFoundError, browser.getLink, "Administer charm recipe"
+        )
         self.assertRaises(
-            Unauthorized, self.getUserBrowser, recipe_url + "/+admin",
-            user=self.person)
+            Unauthorized,
+            self.getUserBrowser,
+            recipe_url + "/+admin",
+            user=self.person,
+        )
 
     def test_admin_recipe(self):
         # Admins can change require_virtualized.
         login("admin@xxxxxxxxxxxxx")
         admin = self.factory.makePerson(
-            member_of=[getUtility(ILaunchpadCelebrities).admin])
+            member_of=[getUtility(ILaunchpadCelebrities).admin]
+        )
         login_person(self.person)
         recipe = self.factory.makeCharmRecipe(registrant=self.person)
         self.assertTrue(recipe.require_virtualized)
@@ -422,40 +508,47 @@ class TestCharmRecipeAdminView(BaseTestCharmRecipeView):
         # Administering a charm recipe sets the date_last_modified property.
         login("admin@xxxxxxxxxxxxx")
         ppa_admin = self.factory.makePerson(
-            member_of=[getUtility(ILaunchpadCelebrities).ppa_admin])
+            member_of=[getUtility(ILaunchpadCelebrities).ppa_admin]
+        )
         login_person(self.person)
         date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, date_created=date_created)
+            registrant=self.person, date_created=date_created
+        )
         login_person(ppa_admin)
         view = CharmRecipeAdminView(recipe, LaunchpadTestRequest())
         view.initialize()
         view.request_action.success({"require_virtualized": False})
         self.assertSqlAttributeEqualsDate(
-            recipe, "date_last_modified", UTC_NOW)
+            recipe, "date_last_modified", UTC_NOW
+        )
 
 
 class TestCharmRecipeEditView(BaseTestCharmRecipeView):
-
     def test_edit_recipe(self):
         [old_git_ref] = self.factory.makeGitRefs()
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person, git_ref=old_git_ref)
+            registrant=self.person, owner=self.person, git_ref=old_git_ref
+        )
         self.factory.makeTeam(
-            name="new-team", displayname="New Team", members=[self.person])
+            name="new-team", displayname="New Team", members=[self.person]
+        )
         [new_git_ref] = self.factory.makeGitRefs()
 
         browser = self.getViewBrowser(recipe, user=self.person)
         browser.getLink("Edit charm recipe").click()
         browser.getControl("Owner").value = ["new-team"]
         browser.getControl(name="field.name").value = "new-name"
-        browser.getControl(name="field.git_ref.repository").value = (
-            new_git_ref.repository.identity)
+        browser.getControl(
+            name="field.git_ref.repository"
+        ).value = new_git_ref.repository.identity
         browser.getControl(name="field.git_ref.path").value = new_git_ref.path
         browser.getControl(
-            "Automatically build when branch changes").selected = True
+            "Automatically build when branch changes"
+        ).selected = True
         browser.getControl(
-            name="field.auto_build_channels.charmcraft").value = "edge"
+            name="field.auto_build_channels.charmcraft"
+        ).value = "edge"
         browser.getControl("Update charm recipe").click()
 
         content = find_main_content(browser.contents)
@@ -463,42 +556,56 @@ class TestCharmRecipeEditView(BaseTestCharmRecipeView):
         self.assertThat("New Team", MatchesPickerText(content, "edit-owner"))
         self.assertThat(
             "Source:\n%s\nEdit charm recipe" % new_git_ref.display_name,
-            MatchesTagText(content, "source"))
+            MatchesTagText(content, "source"),
+        )
         self.assertThat(
             "Build schedule:\n(?)\nBuilt automatically\nEdit charm recipe\n",
-            MatchesTagText(content, "auto_build"))
+            MatchesTagText(content, "auto_build"),
+        )
         self.assertThat(
             "Source snap channels for automatic builds:\nEdit charm recipe\n"
             "charmcraft\nedge",
-            MatchesTagText(content, "auto_build_channels"))
+            MatchesTagText(content, "auto_build_channels"),
+        )
         self.assertThat(
             "Builds of this charm recipe are not automatically uploaded to "
             "the store.\nEdit charm recipe",
-            MatchesTagText(content, "store_upload"))
+            MatchesTagText(content, "store_upload"),
+        )
 
     def test_edit_recipe_sets_date_last_modified(self):
         # Editing a charm recipe sets the date_last_modified property.
         date_created = datetime(2000, 1, 1, tzinfo=pytz.UTC)
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, date_created=date_created)
+            registrant=self.person, date_created=date_created
+        )
         with person_logged_in(self.person):
             view = CharmRecipeEditView(recipe, LaunchpadTestRequest())
             view.initialize()
-            view.request_action.success({
-                "owner": recipe.owner,
-                "name": "changed",
-                })
+            view.request_action.success(
+                {
+                    "owner": recipe.owner,
+                    "name": "changed",
+                }
+            )
         self.assertSqlAttributeEqualsDate(
-            recipe, "date_last_modified", UTC_NOW)
+            recipe, "date_last_modified", UTC_NOW
+        )
 
     def test_edit_recipe_already_exists(self):
         project = self.factory.makeProduct(displayname="Test Project")
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, project=project, owner=self.person,
-            name="one")
+            registrant=self.person,
+            project=project,
+            owner=self.person,
+            name="one",
+        )
         self.factory.makeCharmRecipe(
-            registrant=self.person, project=project, owner=self.person,
-            name="two")
+            registrant=self.person,
+            project=project,
+            owner=self.person,
+            name="two",
+        )
         browser = self.getViewBrowser(recipe, user=self.person)
         browser.getLink("Edit charm recipe").click()
         browser.getControl(name="field.name").value = "two"
@@ -506,14 +613,17 @@ class TestCharmRecipeEditView(BaseTestCharmRecipeView):
         self.assertEqual(
             "There is already a charm recipe owned by Test Person in "
             "Test Project with this name.",
-            extract_text(find_tags_by_class(browser.contents, "message")[1]))
+            extract_text(find_tags_by_class(browser.contents, "message")[1]),
+        )
 
     def test_edit_public_recipe_private_owner(self):
         login_person(self.person)
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person)
+            registrant=self.person, owner=self.person
+        )
         private_team = self.factory.makeTeam(
-            owner=self.person, visibility=PersonVisibility.PRIVATE)
+            owner=self.person, visibility=PersonVisibility.PRIVATE
+        )
         private_team_name = private_team.name
         browser = self.getViewBrowser(recipe, user=self.person)
         browser.getLink("Edit charm recipe").click()
@@ -521,49 +631,60 @@ class TestCharmRecipeEditView(BaseTestCharmRecipeView):
         browser.getControl("Update charm recipe").click()
         self.assertEqual(
             "A public charm recipe cannot have a private owner.",
-            extract_text(find_tags_by_class(browser.contents, "message")[1]))
+            extract_text(find_tags_by_class(browser.contents, "message")[1]),
+        )
 
     def test_edit_public_recipe_private_git_ref(self):
         login_person(self.person)
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person,
-            git_ref=self.factory.makeGitRefs()[0])
+            registrant=self.person,
+            owner=self.person,
+            git_ref=self.factory.makeGitRefs()[0],
+        )
         login_person(self.person)
         [private_ref] = self.factory.makeGitRefs(
-            owner=self.person,
-            information_type=InformationType.PRIVATESECURITY)
+            owner=self.person, information_type=InformationType.PRIVATESECURITY
+        )
         private_ref_identity = private_ref.repository.identity
         private_ref_path = private_ref.path
         browser = self.getViewBrowser(recipe, user=self.person)
         browser.getLink("Edit charm recipe").click()
-        browser.getControl(name="field.git_ref.repository").value = (
-            private_ref_identity)
+        browser.getControl(
+            name="field.git_ref.repository"
+        ).value = private_ref_identity
         browser.getControl(name="field.git_ref.path").value = private_ref_path
         browser.getControl("Update charm recipe").click()
         self.assertEqual(
             "A public charm recipe cannot have a private repository.",
-            extract_text(find_tags_by_class(browser.contents, "message")[1]))
+            extract_text(find_tags_by_class(browser.contents, "message")[1]),
+        )
 
 
 class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
-
     def setUp(self):
         super().setUp()
         self.pushConfig("charms", charmhub_url="http://charmhub.example/";)
         self.pushConfig(
             "launchpad",
             candid_service_root="https://candid.test/";,
-            csrf_secret="test secret")
+            csrf_secret="test secret",
+        )
         self.recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person, store_upload=True,
-            store_name=self.factory.getUniqueUnicode())
+            registrant=self.person,
+            owner=self.person,
+            store_upload=True,
+            store_name=self.factory.getUniqueUnicode(),
+        )
 
     def test_unauthorized(self):
         # A user without edit access cannot authorize charm recipe uploads.
         other_person = self.factory.makePerson()
         self.assertRaises(
-            Unauthorized, self.getUserBrowser,
-            canonical_url(self.recipe) + "/+authorize", user=other_person)
+            Unauthorized,
+            self.getUserBrowser,
+            canonical_url(self.recipe) + "/+authorize",
+            user=other_person,
+        )
 
     @responses.activate
     def test_begin_authorization(self):
@@ -572,93 +693,139 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
         # an existing charm recipe without having to edit it.
         recipe_url = canonical_url(self.recipe)
         owner = self.recipe.owner
+        store_name = self.recipe.store_name
         browser = self.getNonRedirectingBrowser(
-            url=recipe_url + "/+authorize", user=self.recipe.owner)
+            url=recipe_url + "/+authorize", user=self.recipe.owner
+        )
         root_macaroon = Macaroon(version=2)
         root_macaroon.add_third_party_caveat(
-            "https://candid.test/";, "", "identity")
+            "https://candid.test/";, "", "identity"
+        )
         caveat = root_macaroon.caveats[0]
         root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens";,
-            json={"macaroon": root_macaroon_raw})
+            "POST",
+            "http://charmhub.example/v1/tokens";,
+            json={"macaroon": root_macaroon_raw},
+        )
         responses.add(
-            "POST", "https://candid.test/discharge";, status=401,
+            "POST",
+            "https://candid.test/discharge";,
+            status=401,
             json={
                 "Code": "interaction required",
                 "Message": (
-                    "macaroon discharge required: authentication required"),
+                    "macaroon discharge required: authentication required"
+                ),
                 "Info": {
                     "InteractionMethods": {
                         "browser-redirect": {
                             "LoginURL": "https://candid.test/login-redirect";,
-                            },
                         },
                     },
-                })
+                },
+            },
+        )
         browser.getControl("Begin authorization").click()
+        tokens_matcher = MatchesStructure(
+            url=Equals("http://charmhub.example/v1/tokens";),
+            method=Equals("POST"),
+            body=AfterPreprocessing(
+                lambda b: json.loads(b.decode()),
+                Equals(
+                    {
+                        "description": (
+                            "{} for launchpad.test".format(store_name)
+                        ),
+                        "packages": [
+                            {
+                                "type": "charm",
+                                "name": store_name,
+                            },
+                        ],
+                        "permissions": [
+                            "package-manage-releases",
+                            "package-manage-revisions",
+                            "package-view-revisions",
+                        ],
+                    }
+                ),
+            ),
+        )
+        discharge_matcher = MatchesStructure(
+            url=Equals("https://candid.test/discharge";),
+            headers=ContainsDict(
+                {
+                    "Content-Type": Equals(
+                        "application/x-www-form-urlencoded"
+                    ),
+                }
+            ),
+            body=AfterPreprocessing(
+                parse_qs,
+                MatchesDict(
+                    {
+                        "id64": Equals(
+                            [base64.b64encode(caveat.caveat_id_bytes).decode()]
+                        ),
+                    }
+                ),
+            ),
+        )
+        self.assertThat(
+            responses.calls,
+            MatchesListwise(
+                [
+                    MatchesStructure(request=tokens_matcher),
+                    MatchesStructure(request=discharge_matcher),
+                ]
+            ),
+        )
         with person_logged_in(owner):
-            self.assertThat(responses.calls, MatchesListwise([
-                MatchesStructure(
-                    request=MatchesStructure(
-                        url=Equals("http://charmhub.example/v1/tokens";),
-                        method=Equals("POST"),
-                        body=AfterPreprocessing(
-                            lambda b: json.loads(b.decode()),
-                            Equals({
-                                "description": (
-                                    "{} for launchpad.test".format(
-                                        self.recipe.store_name)),
-                                "packages": [
-                                    {"type": "charm",
-                                     "name": self.recipe.store_name},
-                                    ],
-                                "permissions": [
-                                    "package-manage-releases",
-                                    "package-manage-revisions",
-                                    "package-view-revisions",
-                                    ],
-                                })))),
-                MatchesStructure(
-                    request=MatchesStructure(
-                        url=Equals("https://candid.test/discharge";),
-                        headers=ContainsDict({
-                            "Content-Type": Equals(
-                                "application/x-www-form-urlencoded"),
-                            }),
-                        body=AfterPreprocessing(parse_qs, MatchesDict({
-                            "id64": Equals(
-                                [base64.b64encode(
-                                    caveat.caveat_id_bytes).decode()]),
-                            })))),
-                ]))
             self.assertEqual(
-                {"root": root_macaroon_raw}, self.recipe.store_secrets)
+                {"root": root_macaroon_raw}, self.recipe.store_secrets
+            )
         self.assertEqual(303, int(browser.headers["Status"].split(" ", 1)[0]))
+        return_to_matcher = AfterPreprocessing(
+            urlsplit,
+            MatchesStructure(
+                scheme=Equals("http"),
+                netloc=Equals("launchpad.test"),
+                path=Equals("/+candid-callback"),
+                query=AfterPreprocessing(
+                    parse_qs,
+                    MatchesDict(
+                        {
+                            "starting_url": Equals(
+                                [recipe_url + "/+authorize"]
+                            ),
+                            "discharge_macaroon_action": Equals(
+                                ["field.actions.complete"]
+                            ),
+                            "discharge_macaroon_field": Equals(
+                                ["field.discharge_macaroon"]
+                            ),
+                        }
+                    ),
+                ),
+                fragment=Equals(""),
+            ),
+        )
         self.assertThat(
             urlsplit(browser.headers["Location"]),
             MatchesStructure(
                 scheme=Equals("https"),
                 netloc=Equals("candid.test"),
                 path=Equals("/login-redirect"),
-                query=AfterPreprocessing(parse_qs, ContainsDict({
-                    "return_to": MatchesListwise([
-                        AfterPreprocessing(urlsplit, MatchesStructure(
-                            scheme=Equals("http"),
-                            netloc=Equals("launchpad.test"),
-                            path=Equals("/+candid-callback"),
-                            query=AfterPreprocessing(parse_qs, MatchesDict({
-                                "starting_url": Equals(
-                                    [recipe_url + "/+authorize"]),
-                                "discharge_macaroon_action": Equals(
-                                    ["field.actions.complete"]),
-                                "discharge_macaroon_field": Equals(
-                                    ["field.discharge_macaroon"]),
-                                })),
-                            fragment=Equals(""))),
-                        ]),
-                    })),
-                fragment=Equals("")))
+                query=AfterPreprocessing(
+                    parse_qs,
+                    ContainsDict(
+                        {"return_to": MatchesListwise([return_to_matcher])}
+                    ),
+                ),
+                fragment=Equals(""),
+            ),
+        )
 
     def test_complete_authorization_missing_discharge_macaroon(self):
         # If the form does not include a discharge macaroon, the "complete"
@@ -666,17 +833,22 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
         with person_logged_in(self.recipe.owner):
             self.recipe.store_secrets = {
                 "root": Macaroon(version=2).serialize(JsonSerializer()),
-                }
+            }
             transaction.commit()
             form = {"field.actions.complete": "1"}
             view = create_initialized_view(
-                self.recipe, "+authorize", form=form, method="POST",
-                principal=self.recipe.owner)
+                self.recipe,
+                "+authorize",
+                form=form,
+                method="POST",
+                principal=self.recipe.owner,
+            )
             html = view()
             self.assertEqual(
-                "Uploads of %s to Charmhub were not authorized." %
-                self.recipe.name,
-                get_feedback_messages(html)[1])
+                "Uploads of %s to Charmhub were not authorized."
+                % self.recipe.name,
+                get_feedback_messages(html)[1],
+            )
             self.assertNotIn("exchanged_encrypted", self.recipe.store_secrets)
 
     @responses.activate
@@ -688,86 +860,122 @@ class TestCharmRecipeAuthorizeView(BaseTestCharmRecipeView):
         self.pushConfig(
             "charms",
             charmhub_secrets_public_key=base64.b64encode(
-                bytes(private_key.public_key)).decode())
+                bytes(private_key.public_key)
+            ).decode(),
+        )
         root_macaroon = Macaroon(version=2)
         root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
         unbound_discharge_macaroon = Macaroon(version=2)
         unbound_discharge_macaroon_raw = unbound_discharge_macaroon.serialize(
-            JsonSerializer())
+            JsonSerializer()
+        )
         discharge_macaroon_raw = root_macaroon.prepare_for_request(
-            unbound_discharge_macaroon).serialize(JsonSerializer())
+            unbound_discharge_macaroon
+        ).serialize(JsonSerializer())
         exchanged_macaroon = Macaroon(version=2)
         exchanged_macaroon_raw = exchanged_macaroon.serialize(JsonSerializer())
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens/exchange";,
-            json={"macaroon": exchanged_macaroon_raw})
+            "POST",
+            "http://charmhub.example/v1/tokens/exchange";,
+            json={"macaroon": exchanged_macaroon_raw},
+        )
         with person_logged_in(self.recipe.owner):
             self.recipe.store_secrets = {"root": root_macaroon_raw}
             transaction.commit()
             form = {
                 "field.actions.complete": "1",
                 "field.discharge_macaroon": unbound_discharge_macaroon_raw,
-                }
+            }
             view = create_initialized_view(
-                self.recipe, "+authorize", form=form, method="POST",
-                principal=self.recipe.owner)
+                self.recipe,
+                "+authorize",
+                form=form,
+                method="POST",
+                principal=self.recipe.owner,
+            )
             self.assertEqual(302, view.request.response.getStatus())
             self.assertEqual(
                 canonical_url(self.recipe),
-                view.request.response.getHeader("Location"))
+                view.request.response.getHeader("Location"),
+            )
             self.assertEqual(
-                "Uploads of %s to Charmhub are now authorized." %
-                self.recipe.name,
-                view.request.response.notifications[0].message)
+                "Uploads of %s to Charmhub are now authorized."
+                % self.recipe.name,
+                view.request.response.notifications[0].message,
+            )
             self.pushConfig(
                 "charms",
                 charmhub_secrets_private_key=base64.b64encode(
-                    bytes(private_key)).decode())
+                    bytes(private_key)
+                ).decode(),
+            )
             container = getUtility(IEncryptedContainer, "charmhub-secrets")
-            self.assertThat(self.recipe.store_secrets, MatchesDict({
-                "exchanged_encrypted": AfterPreprocessing(
-                    lambda data: container.decrypt(data).decode(),
-                    Equals(exchanged_macaroon_raw)),
-                }))
-        self.assertThat(responses.calls, MatchesListwise([
-            MatchesStructure(
-                request=MatchesStructure(
-                    url=Equals("http://charmhub.example/v1/tokens/exchange";),
-                    method=Equals("POST"),
-                    headers=ContainsDict({
-                        "Macaroons": AfterPreprocessing(
-                            lambda v: json.loads(
-                                base64.b64decode(v.encode()).decode()),
-                            Equals([
-                                json.loads(m) for m in (
+            self.assertThat(
+                self.recipe.store_secrets,
+                MatchesDict(
+                    {
+                        "exchanged_encrypted": AfterPreprocessing(
+                            lambda data: container.decrypt(data).decode(),
+                            Equals(exchanged_macaroon_raw),
+                        ),
+                    }
+                ),
+            )
+        exchange_matcher = MatchesStructure(
+            url=Equals("http://charmhub.example/v1/tokens/exchange";),
+            method=Equals("POST"),
+            headers=ContainsDict(
+                {
+                    "Macaroons": AfterPreprocessing(
+                        lambda v: json.loads(
+                            base64.b64decode(v.encode()).decode()
+                        ),
+                        Equals(
+                            [
+                                json.loads(m)
+                                for m in (
                                     root_macaroon_raw,
-                                    discharge_macaroon_raw)])),
-                        }),
-                    body=AfterPreprocessing(
-                        lambda b: json.loads(b.decode()),
-                        Equals({})))),
-            ]))
+                                    discharge_macaroon_raw,
+                                )
+                            ]
+                        ),
+                    ),
+                }
+            ),
+            body=AfterPreprocessing(
+                lambda b: json.loads(b.decode()), Equals({})
+            ),
+        )
+        self.assertThat(
+            responses.calls,
+            MatchesListwise([MatchesStructure(request=exchange_matcher)]),
+        )
 
 
 class TestCharmRecipeDeleteView(BaseTestCharmRecipeView):
-
     def test_unauthorized(self):
         # A user without edit access cannot delete a charm recipe.
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person)
+            registrant=self.person, owner=self.person
+        )
         recipe_url = canonical_url(recipe)
         other_person = self.factory.makePerson()
         browser = self.getViewBrowser(recipe, user=other_person)
         self.assertRaises(
-            LinkNotFoundError, browser.getLink, "Delete charm recipe")
+            LinkNotFoundError, browser.getLink, "Delete charm recipe"
+        )
         self.assertRaises(
-            Unauthorized, self.getUserBrowser, recipe_url + "/+delete",
-            user=other_person)
+            Unauthorized,
+            self.getUserBrowser,
+            recipe_url + "/+delete",
+            user=other_person,
+        )
 
     def test_delete_recipe_without_builds(self):
         # A charm recipe without builds can be deleted.
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person)
+            registrant=self.person, owner=self.person
+        )
         recipe_url = canonical_url(recipe)
         owner_url = canonical_url(self.person)
         browser = self.getViewBrowser(recipe, user=self.person)
@@ -779,7 +987,8 @@ class TestCharmRecipeDeleteView(BaseTestCharmRecipeView):
     def test_delete_recipe_with_builds(self):
         # A charm recipe with builds can be deleted.
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person)
+            registrant=self.person, owner=self.person
+        )
         build = self.factory.makeCharmRecipeBuild(recipe=recipe)
         self.factory.makeCharmFile(build=build)
         recipe_url = canonical_url(recipe)
@@ -792,18 +1001,21 @@ class TestCharmRecipeDeleteView(BaseTestCharmRecipeView):
 
 
 class TestCharmRecipeView(BaseTestCharmRecipeView):
-
     def setUp(self):
         super().setUp()
         self.project = self.factory.makeProduct(
-            name="test-project", displayname="Test Project")
+            name="test-project", displayname="Test Project"
+        )
         self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
         self.distroseries = self.factory.makeDistroSeries(
-            distribution=self.ubuntu)
+            distribution=self.ubuntu
+        )
         processor = getUtility(IProcessorSet).getByName("386")
         self.distroarchseries = self.factory.makeDistroArchSeries(
-            distroseries=self.distroseries, architecturetag="i386",
-            processor=processor)
+            distroseries=self.distroseries,
+            architecturetag="i386",
+            processor=processor,
+        )
         self.factory.makeBuilder(virtualized=True)
 
     def makeCharmRecipe(self, **kwargs):
@@ -812,8 +1024,11 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
         if "git_ref" not in kwargs:
             kwargs["git_ref"] = self.factory.makeGitRefs()[0]
         return self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person, name="charm-name",
-            **kwargs)
+            registrant=self.person,
+            owner=self.person,
+            name="charm-name",
+            **kwargs,
+        )
 
     def makeBuild(self, recipe=None, date_created=None, **kwargs):
         if recipe is None:
@@ -821,11 +1036,15 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
         if date_created is None:
             date_created = datetime.now(pytz.UTC) - timedelta(hours=1)
         build = self.factory.makeCharmRecipeBuild(
-            requester=self.person, recipe=recipe,
+            requester=self.person,
+            recipe=recipe,
             distro_arch_series=self.distroarchseries,
-            date_created=date_created, **kwargs)
+            date_created=date_created,
+            **kwargs,
+        )
         job = removeSecurityProxy(
-            removeSecurityProxy(build.build_request)._job)
+            removeSecurityProxy(build.build_request)._job
+        )
         job.job._status = JobStatus.COMPLETED
         return build
 
@@ -834,34 +1053,53 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
         view = create_view(recipe, "+index")
         # To test the breadcrumbs we need a correct traversal stack.
         view.request.traversed_objects = [
-            recipe.owner, recipe.project, recipe, view]
+            recipe.owner,
+            recipe.project,
+            recipe,
+            view,
+        ]
         view.initialize()
         breadcrumbs_tag = soupmatchers.Tag(
-            "breadcrumbs", "ol", attrs={"class": "breadcrumbs"})
+            "breadcrumbs", "ol", attrs={"class": "breadcrumbs"}
+        )
         self.assertThat(
             view(),
             soupmatchers.HTMLContains(
                 soupmatchers.Within(
                     breadcrumbs_tag,
                     soupmatchers.Tag(
-                        "project breadcrumb", "a",
+                        "project breadcrumb",
+                        "a",
                         text="Test Project",
-                        attrs={"href": re.compile(r"/test-project$")})),
+                        attrs={"href": re.compile(r"/test-project$")},
+                    ),
+                ),
                 soupmatchers.Within(
                     breadcrumbs_tag,
                     soupmatchers.Tag(
-                        "charm breadcrumb", "li",
-                        text=re.compile(r"\scharm-name\s")))))
+                        "charm breadcrumb",
+                        "li",
+                        text=re.compile(r"\scharm-name\s"),
+                    ),
+                ),
+            ),
+        )
 
     def test_index_git(self):
         [ref] = self.factory.makeGitRefs(
-            owner=self.person, target=self.project, name="charm-repository",
-            paths=["refs/heads/master"])
+            owner=self.person,
+            target=self.project,
+            name="charm-repository",
+            paths=["refs/heads/master"],
+        )
         recipe = self.makeCharmRecipe(git_ref=ref)
         build = self.makeBuild(
-            recipe=recipe, status=BuildStatus.FULLYBUILT,
-            duration=timedelta(minutes=30))
-        self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+            recipe=recipe,
+            status=BuildStatus.FULLYBUILT,
+            duration=timedelta(minutes=30),
+        )
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            r"""\
             Test Project
             charm-name
             .*
@@ -876,46 +1114,59 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
             Latest builds
             Status When complete Architecture
             Successfully built 30 minutes ago i386
-            """, self.getMainText(build.recipe))
+            """,
+            self.getMainText(build.recipe),
+        )
 
     def test_index_success_with_buildlog(self):
         # The build log is shown if it is there.
         build = self.makeBuild(
-            status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30))
+            status=BuildStatus.FULLYBUILT, duration=timedelta(minutes=30)
+        )
         build.setLog(self.factory.makeLibraryFileAlias())
-        self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            r"""\
             Latest builds
             Status When complete Architecture
             Successfully built 30 minutes ago buildlog \(.*\) i386
-            """, self.getMainText(build.recipe))
+            """,
+            self.getMainText(build.recipe),
+        )
 
     def test_index_no_builds(self):
         # A message is shown when there are no builds.
         recipe = self.makeCharmRecipe()
         self.assertIn(
             "This charm recipe has not been built yet.",
-            self.getMainText(recipe))
+            self.getMainText(recipe),
+        )
 
     def test_index_pending_build(self):
         # A pending build is listed as such.
         build = self.makeBuild()
         build.queueBuild()
-        self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            r"""\
             Latest builds
             Status When complete Architecture
             Needs building in .* \(estimated\) i386
-            """, self.getMainText(build.recipe))
+            """,
+            self.getMainText(build.recipe),
+        )
 
     def test_index_pending_build_request(self):
         # A pending build request is listed as such.
         recipe = self.makeCharmRecipe()
         with person_logged_in(recipe.owner):
             recipe.requestBuilds(recipe.owner)
-        self.assertTextMatchesExpressionIgnoreWhitespace("""\
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            """\
             Latest builds
             Status When complete Architecture
             Pending build request
-            """, self.getMainText(recipe))
+            """,
+            self.getMainText(recipe),
+        )
 
     def test_index_failed_build_request(self):
         # A failed build request is listed as such, with its error message.
@@ -926,17 +1177,22 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
         job.job._status = JobStatus.FAILED
         job.job.date_finished = datetime.now(pytz.UTC) - timedelta(hours=1)
         job.error_message = "Boom"
-        self.assertTextMatchesExpressionIgnoreWhitespace(r"""\
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            r"""\
             Latest builds
             Status When complete Architecture
             Failed build request 1 hour ago \(Boom\)
-            """, self.getMainText(recipe))
+            """,
+            self.getMainText(recipe),
+        )
 
     def setStatus(self, build, status):
         build.updateStatus(
-            BuildStatus.BUILDING, date_started=build.date_created)
+            BuildStatus.BUILDING, date_started=build.date_created
+        )
         build.updateStatus(
-            status, date_finished=build.date_started + timedelta(minutes=30))
+            status, date_finished=build.date_started + timedelta(minutes=30)
+        )
 
     def test_builds_and_requests(self):
         # CharmRecipeView.builds_and_requests produces reasonable results,
@@ -944,10 +1200,12 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
         recipe = self.makeCharmRecipe()
         # Create oldest builds first so that they sort properly by id.
         date_gen = time_counter(
-            datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1))
+            datetime(2000, 1, 1, tzinfo=pytz.UTC), timedelta(days=1)
+        )
         builds = [
             self.makeBuild(recipe=recipe, date_created=next(date_gen))
-            for i in range(3)]
+            for i in range(3)
+        ]
         self.setStatus(builds[2], BuildStatus.FULLYBUILT)
         with person_logged_in(recipe.owner):
             request = recipe.requestBuilds(recipe.owner)
@@ -956,34 +1214,49 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
         view = CharmRecipeView(recipe, None)
         # The pending build request is interleaved in date order with
         # pending builds, and these are followed by completed builds.
-        self.assertThat(view.builds_and_requests, MatchesListwise([
-            MatchesStructure.byEquality(id=request.id),
-            Equals(builds[1]),
-            Equals(builds[0]),
-            Equals(builds[2]),
-            ]))
+        self.assertThat(
+            view.builds_and_requests,
+            MatchesListwise(
+                [
+                    MatchesStructure.byEquality(id=request.id),
+                    Equals(builds[1]),
+                    Equals(builds[0]),
+                    Equals(builds[2]),
+                ]
+            ),
+        )
         transaction.commit()
         builds.append(self.makeBuild(recipe=recipe))
         del get_property_cache(view).builds_and_requests
-        self.assertThat(view.builds_and_requests, MatchesListwise([
-            Equals(builds[3]),
-            MatchesStructure.byEquality(id=request.id),
-            Equals(builds[1]),
-            Equals(builds[0]),
-            Equals(builds[2]),
-            ]))
+        self.assertThat(
+            view.builds_and_requests,
+            MatchesListwise(
+                [
+                    Equals(builds[3]),
+                    MatchesStructure.byEquality(id=request.id),
+                    Equals(builds[1]),
+                    Equals(builds[0]),
+                    Equals(builds[2]),
+                ]
+            ),
+        )
         # If we pretend that the job failed, it is still listed, but after
         # any pending builds.
         job.job._status = JobStatus.FAILED
         job.job.date_finished = job.date_created + timedelta(minutes=30)
         del get_property_cache(view).builds_and_requests
-        self.assertThat(view.builds_and_requests, MatchesListwise([
-            Equals(builds[3]),
-            Equals(builds[1]),
-            Equals(builds[0]),
-            MatchesStructure.byEquality(id=request.id),
-            Equals(builds[2]),
-            ]))
+        self.assertThat(
+            view.builds_and_requests,
+            MatchesListwise(
+                [
+                    Equals(builds[3]),
+                    Equals(builds[1]),
+                    Equals(builds[0]),
+                    MatchesStructure.byEquality(id=request.id),
+                    Equals(builds[2]),
+                ]
+            ),
+        )
 
     def test_store_channels_empty(self):
         recipe = self.factory.makeCharmRecipe()
@@ -992,35 +1265,44 @@ class TestCharmRecipeView(BaseTestCharmRecipeView):
 
     def test_store_channels_display(self):
         recipe = self.factory.makeCharmRecipe(
-            store_channels=["track/stable/fix-123", "track/edge/fix-123"])
+            store_channels=["track/stable/fix-123", "track/edge/fix-123"]
+        )
         view = create_initialized_view(recipe, "+index")
         self.assertEqual(
-            "track/stable/fix-123, track/edge/fix-123", view.store_channels)
+            "track/stable/fix-123, track/edge/fix-123", view.store_channels
+        )
 
 
 class TestCharmRecipeRequestBuildsView(BaseTestCharmRecipeView):
-
     def setUp(self):
         super().setUp()
         self.project = self.factory.makeProduct(
-            name="test-project", displayname="Test Project")
+            name="test-project", displayname="Test Project"
+        )
         self.ubuntu = getUtility(ILaunchpadCelebrities).ubuntu
         self.distroseries = self.factory.makeDistroSeries(
-            distribution=self.ubuntu)
+            distribution=self.ubuntu
+        )
         self.architectures = []
         for processor, architecture in ("386", "i386"), ("amd64", "amd64"):
             das = self.factory.makeDistroArchSeries(
-                distroseries=self.distroseries, architecturetag=architecture,
-                processor=getUtility(IProcessorSet).getByName(processor))
+                distroseries=self.distroseries,
+                architecturetag=architecture,
+                processor=getUtility(IProcessorSet).getByName(processor),
+            )
             das.addOrUpdateChroot(self.factory.makeLibraryFileAlias())
             self.architectures.append(das)
         self.recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person, project=self.project,
-            name="charm-name")
+            registrant=self.person,
+            owner=self.person,
+            project=self.project,
+            name="charm-name",
+        )
 
     def test_request_builds_page(self):
         # The +request-builds page is sensible.
-        self.assertTextMatchesExpressionIgnoreWhitespace(r"""
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            r"""
             Request builds for charm-name
             Test Project
             charm-name
@@ -1036,36 +1318,45 @@ class TestCharmRecipeRequestBuildsView(BaseTestCharmRecipeView):
             or
             Cancel
             """,
-            self.getMainText(self.recipe, "+request-builds", user=self.person))
+            self.getMainText(self.recipe, "+request-builds", user=self.person),
+        )
 
     def test_request_builds_not_owner(self):
         # A user without launchpad.Edit cannot request builds.
         self.assertRaises(
-            Unauthorized, self.getViewBrowser, self.recipe, "+request-builds")
+            Unauthorized, self.getViewBrowser, self.recipe, "+request-builds"
+        )
 
     def test_request_builds_action(self):
         # Requesting a build creates a pending build request.
         browser = self.getViewBrowser(
-            self.recipe, "+request-builds", user=self.person)
+            self.recipe, "+request-builds", user=self.person
+        )
         browser.getControl("Request builds").click()
 
         login_person(self.person)
         [request] = self.recipe.pending_build_requests
-        self.assertThat(removeSecurityProxy(request), MatchesStructure(
-            recipe=Equals(self.recipe),
-            status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-            error_message=Is(None),
-            builds=AfterPreprocessing(list, Equals([])),
-            _job=MatchesStructure(
-                requester=Equals(self.person),
-                channels=Equals({}),
-                architectures=Is(None))))
+        self.assertThat(
+            removeSecurityProxy(request),
+            MatchesStructure(
+                recipe=Equals(self.recipe),
+                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                error_message=Is(None),
+                builds=AfterPreprocessing(list, Equals([])),
+                _job=MatchesStructure(
+                    requester=Equals(self.person),
+                    channels=Equals({}),
+                    architectures=Is(None),
+                ),
+            ),
+        )
 
     def test_request_builds_channels(self):
         # Selecting different channels creates a build request using those
         # channels.
         browser = self.getViewBrowser(
-            self.recipe, "+request-builds", user=self.person)
+            self.recipe, "+request-builds", user=self.person
+        )
         browser.getControl(name="field.channels.core").value = "edge"
         browser.getControl("Request builds").click()
 
diff --git a/lib/lp/charms/browser/tests/test_charmrecipebuild.py b/lib/lp/charms/browser/tests/test_charmrecipebuild.py
index 0cbc484..83df915 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipebuild.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipebuild.py
@@ -5,12 +5,12 @@
 
 import re
 
+import soupmatchers
+import transaction
 from fixtures import FakeLogger
 from pymacaroons import Macaroon
-import soupmatchers
 from storm.locals import Store
 from testtools.matchers import StartsWith
-import transaction
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
@@ -27,19 +27,16 @@ from lp.services.webapp import canonical_url
 from lp.testing import (
     ANONYMOUS,
     BrowserTestCase,
+    TestCaseWithFactory,
     login,
     person_logged_in,
-    TestCaseWithFactory,
-    )
-from lp.testing.layers import (
-    DatabaseFunctionalLayer,
-    LaunchpadFunctionalLayer,
-    )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, LaunchpadFunctionalLayer
 from lp.testing.pages import (
     extract_text,
     find_main_content,
     find_tags_by_class,
-    )
+)
 from lp.testing.views import create_initialized_view
 
 
@@ -55,14 +52,18 @@ class TestCanonicalUrlForCharmRecipeBuild(TestCaseWithFactory):
         owner = self.factory.makePerson(name="person")
         project = self.factory.makeProduct(name="charm-project")
         recipe = self.factory.makeCharmRecipe(
-            registrant=owner, owner=owner, project=project, name="charm")
+            registrant=owner, owner=owner, project=project, name="charm"
+        )
         build = self.factory.makeCharmRecipeBuild(
-            requester=owner, recipe=recipe)
+            requester=owner, recipe=recipe
+        )
         self.assertThat(
             canonical_url(build),
             StartsWith(
                 "http://launchpad.test/~person/charm-project/+charm/charm/";
-                "+build/"))
+                "+build/"
+            ),
+        )
 
 
 class TestCharmRecipeBuildView(TestCaseWithFactory):
@@ -76,12 +77,14 @@ class TestCharmRecipeBuildView(TestCaseWithFactory):
     def test_files(self):
         # CharmRecipeBuildView.files returns all the associated files.
         build = self.factory.makeCharmRecipeBuild(
-            status=BuildStatus.FULLYBUILT)
+            status=BuildStatus.FULLYBUILT
+        )
         charm_file = self.factory.makeCharmFile(build=build)
         build_view = create_initialized_view(build, "+index")
         self.assertEqual(
             [charm_file.library_file.filename],
-            [lfa.filename for lfa in build_view.files])
+            [lfa.filename for lfa in build_view.files],
+        )
         # Deleted files won't be included.
         self.assertFalse(charm_file.library_file.deleted)
         removeSecurityProxy(charm_file.library_file).content = None
@@ -92,69 +95,106 @@ class TestCharmRecipeBuildView(TestCaseWithFactory):
     def test_revision_id(self):
         build = self.factory.makeCharmRecipeBuild()
         build.updateStatus(
-            BuildStatus.FULLYBUILT, worker_status={"revision_id": "dummy"})
+            BuildStatus.FULLYBUILT, worker_status={"revision_id": "dummy"}
+        )
         build_view = create_initialized_view(build, "+index")
-        self.assertThat(build_view(), soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "revision ID", "li", attrs={"id": "revision-id"},
-                text=re.compile(r"^\s*Revision: dummy\s*$"))))
+        self.assertThat(
+            build_view(),
+            soupmatchers.HTMLContains(
+                soupmatchers.Tag(
+                    "revision ID",
+                    "li",
+                    attrs={"id": "revision-id"},
+                    text=re.compile(r"^\s*Revision: dummy\s*$"),
+                )
+            ),
+        )
 
     def test_store_upload_status_in_progress(self):
         build = self.factory.makeCharmRecipeBuild(
-            status=BuildStatus.FULLYBUILT)
+            status=BuildStatus.FULLYBUILT
+        )
         getUtility(ICharmhubUploadJobSource).create(build)
         build_view = create_initialized_view(build, "+index")
-        self.assertThat(build_view(), soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "store upload status", "li",
-                attrs={"id": "store-upload-status"},
-                text=re.compile(r"^\s*Charmhub upload in progress\s*$"))))
+        self.assertThat(
+            build_view(),
+            soupmatchers.HTMLContains(
+                soupmatchers.Tag(
+                    "store upload status",
+                    "li",
+                    attrs={"id": "store-upload-status"},
+                    text=re.compile(r"^\s*Charmhub upload in progress\s*$"),
+                )
+            ),
+        )
 
     def test_store_upload_status_completed(self):
         build = self.factory.makeCharmRecipeBuild(
-            status=BuildStatus.FULLYBUILT)
+            status=BuildStatus.FULLYBUILT
+        )
         job = getUtility(ICharmhubUploadJobSource).create(build)
         naked_job = removeSecurityProxy(job)
         naked_job.job._status = JobStatus.COMPLETED
         build_view = create_initialized_view(build, "+index")
-        self.assertThat(build_view(), soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "store upload status", "li",
-                attrs={"id": "store-upload-status"},
-                text=re.compile(r"^\s*Uploaded to Charmhub\s*$"))))
+        self.assertThat(
+            build_view(),
+            soupmatchers.HTMLContains(
+                soupmatchers.Tag(
+                    "store upload status",
+                    "li",
+                    attrs={"id": "store-upload-status"},
+                    text=re.compile(r"^\s*Uploaded to Charmhub\s*$"),
+                )
+            ),
+        )
 
     def test_store_upload_status_failed(self):
         build = self.factory.makeCharmRecipeBuild(
-            status=BuildStatus.FULLYBUILT)
+            status=BuildStatus.FULLYBUILT
+        )
         job = getUtility(ICharmhubUploadJobSource).create(build)
         naked_job = removeSecurityProxy(job)
         naked_job.job._status = JobStatus.FAILED
         naked_job.error_message = "Review failed."
         build_view = create_initialized_view(build, "+index")
-        self.assertThat(build_view(), soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "store upload status", "li",
-                attrs={"id": "store-upload-status"},
-                text=re.compile(
-                    r"^\s*Charmhub upload failed:\s+"
-                    r"Review failed.\s*$"))))
+        self.assertThat(
+            build_view(),
+            soupmatchers.HTMLContains(
+                soupmatchers.Tag(
+                    "store upload status",
+                    "li",
+                    attrs={"id": "store-upload-status"},
+                    text=re.compile(
+                        r"^\s*Charmhub upload failed:\s+" r"Review failed.\s*$"
+                    ),
+                )
+            ),
+        )
 
     def test_store_upload_status_release_failed(self):
         build = self.factory.makeCharmRecipeBuild(
-            status=BuildStatus.FULLYBUILT)
+            status=BuildStatus.FULLYBUILT
+        )
         job = getUtility(ICharmhubUploadJobSource).create(build)
         naked_job = removeSecurityProxy(job)
         naked_job.job._status = JobStatus.FAILED
         naked_job.store_revision = 1
         naked_job.error_message = "Failed to publish"
         build_view = create_initialized_view(build, "+index")
-        self.assertThat(build_view(), soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "store upload status", "li",
-                attrs={"id": "store-upload-status"},
-                text=re.compile(
-                    r"^\s*Charmhub release failed:\s+"
-                    r"Failed to publish\s*$"))))
+        self.assertThat(
+            build_view(),
+            soupmatchers.HTMLContains(
+                soupmatchers.Tag(
+                    "store upload status",
+                    "li",
+                    attrs={"id": "store-upload-status"},
+                    text=re.compile(
+                        r"^\s*Charmhub release failed:\s+"
+                        r"Failed to publish\s*$"
+                    ),
+                )
+            ),
+        )
 
 
 class TestCharmRecipeBuildOperations(BrowserTestCase):
@@ -169,7 +209,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         self.build_url = canonical_url(self.build)
         self.requester = self.build.requester
         self.buildd_admin = self.factory.makePerson(
-            member_of=[getUtility(ILaunchpadCelebrities).buildd_admin])
+            member_of=[getUtility(ILaunchpadCelebrities).buildd_admin]
+        )
 
     def test_retry_build(self):
         # The requester of a build can retry it.
@@ -190,10 +231,14 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         user = self.factory.makePerson()
         browser = self.getViewBrowser(self.build, user=user)
         self.assertRaises(
-            LinkNotFoundError, browser.getLink, "Retry this build")
+            LinkNotFoundError, browser.getLink, "Retry this build"
+        )
         self.assertRaises(
-            Unauthorized, self.getUserBrowser, self.build_url + "/+retry",
-            user=user)
+            Unauthorized,
+            self.getUserBrowser,
+            self.build_url + "/+retry",
+            user=user,
+        )
 
     def test_retry_build_wrong_state(self):
         # If the build isn't in an unsuccessful terminal state, you can't
@@ -201,7 +246,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         self.build.updateStatus(BuildStatus.FULLYBUILT)
         browser = self.getViewBrowser(self.build, user=self.requester)
         self.assertRaises(
-            LinkNotFoundError, browser.getLink, "Retry this build")
+            LinkNotFoundError, browser.getLink, "Retry this build"
+        )
 
     def test_cancel_build(self):
         # The requester of a build can cancel it.
@@ -223,8 +269,11 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         browser = self.getViewBrowser(self.build, user=user)
         self.assertRaises(LinkNotFoundError, browser.getLink, "Cancel build")
         self.assertRaises(
-            Unauthorized, self.getUserBrowser, self.build_url + "/+cancel",
-            user=user)
+            Unauthorized,
+            self.getUserBrowser,
+            self.build_url + "/+cancel",
+            user=user,
+        )
 
     def test_cancel_build_wrong_state(self):
         # If the build isn't queued, you can't cancel it.
@@ -255,7 +304,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         browser.getControl("Rescore build").click()
         self.assertEqual(
             "Invalid integer data",
-            extract_text(find_tags_by_class(browser.contents, "message")[1]))
+            extract_text(find_tags_by_class(browser.contents, "message")[1]),
+        )
 
     def test_rescore_build_not_admin(self):
         # A non-admin user cannot cancel a build.
@@ -265,8 +315,11 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         browser = self.getViewBrowser(self.build, user=user)
         self.assertRaises(LinkNotFoundError, browser.getLink, "Rescore build")
         self.assertRaises(
-            Unauthorized, self.getUserBrowser, self.build_url + "/+rescore",
-            user=user)
+            Unauthorized,
+            self.getUserBrowser,
+            self.build_url + "/+rescore",
+            user=user,
+        )
 
     def test_rescore_build_wrong_state(self):
         # If the build isn't NEEDSBUILD, you can't rescore it.
@@ -283,12 +336,20 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         with person_logged_in(self.requester):
             self.build.cancel()
         browser = self.getViewBrowser(
-            self.build, "+rescore", user=self.buildd_admin)
+            self.build, "+rescore", user=self.buildd_admin
+        )
         self.assertEqual(self.build_url, browser.url)
-        self.assertThat(browser.contents, soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "notification", "div", attrs={"class": "warning message"},
-                text="Cannot rescore this build because it is not queued.")))
+        self.assertThat(
+            browser.contents,
+            soupmatchers.HTMLContains(
+                soupmatchers.Tag(
+                    "notification",
+                    "div",
+                    attrs={"class": "warning message"},
+                    text="Cannot rescore this build because it is not queued.",
+                )
+            ),
+        )
 
     def setUpStoreUpload(self):
         self.pushConfig("charms", charmhub_url="http://charmhub.example/";)
@@ -298,7 +359,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
             # "exchanged_encrypted" is present, so don't bother setting up
             # encryption keys here.
             self.build.recipe.store_secrets = {
-                "exchanged_encrypted": Macaroon().serialize()}
+                "exchanged_encrypted": Macaroon().serialize()
+            }
 
     def test_store_upload(self):
         # A build not previously uploaded to Charmhub can be uploaded
@@ -307,7 +369,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         self.build.updateStatus(BuildStatus.FULLYBUILT)
         self.factory.makeCharmFile(
             build=self.build,
-            library_file=self.factory.makeLibraryFileAlias(db_only=True))
+            library_file=self.factory.makeLibraryFileAlias(db_only=True),
+        )
         browser = self.getViewBrowser(self.build, user=self.requester)
         browser.getControl("Upload this charm to Charmhub").click()
         self.assertEqual(self.build_url, browser.url)
@@ -317,7 +380,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         self.assertEqual(self.build, job.build)
         self.assertEqual(
             "An upload has been scheduled and will run as soon as possible.",
-            extract_text(find_tags_by_class(browser.contents, "message")[0]))
+            extract_text(find_tags_by_class(browser.contents, "message")[0]),
+        )
 
     def test_store_upload_retry(self):
         # A build with a previously-failed Charmhub upload can have the
@@ -326,7 +390,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         self.build.updateStatus(BuildStatus.FULLYBUILT)
         self.factory.makeCharmFile(
             build=self.build,
-            library_file=self.factory.makeLibraryFileAlias(db_only=True))
+            library_file=self.factory.makeLibraryFileAlias(db_only=True),
+        )
         old_job = getUtility(ICharmhubUploadJobSource).create(self.build)
         removeSecurityProxy(old_job).job._status = JobStatus.FAILED
         browser = self.getViewBrowser(self.build, user=self.requester)
@@ -338,7 +403,8 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         self.assertEqual(self.build, job.build)
         self.assertEqual(
             "An upload has been scheduled and will run as soon as possible.",
-            extract_text(find_tags_by_class(browser.contents, "message")[0]))
+            extract_text(find_tags_by_class(browser.contents, "message")[0]),
+        )
 
     def test_store_upload_error_notifies(self):
         # If a build cannot be scheduled for uploading to Charmhub, we issue
@@ -350,26 +416,31 @@ class TestCharmRecipeBuildOperations(BrowserTestCase):
         self.assertEqual(self.build_url, browser.url)
         login(ANONYMOUS)
         self.assertEqual(
-            [], list(getUtility(ICharmhubUploadJobSource).iterReady()))
+            [], list(getUtility(ICharmhubUploadJobSource).iterReady())
+        )
         self.assertEqual(
             "Cannot upload this charm because it has no files.",
-            extract_text(find_tags_by_class(browser.contents, "message")[0]))
+            extract_text(find_tags_by_class(browser.contents, "message")[0]),
+        )
 
     def test_builder_history(self):
         Store.of(self.build).flush()
         self.build.updateStatus(
-            BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder())
+            BuildStatus.FULLYBUILT, builder=self.factory.makeBuilder()
+        )
         title = self.build.title
         browser = self.getViewBrowser(self.build.builder, "+history")
         self.assertTextMatchesExpressionIgnoreWhitespace(
             r"Build history.*%s" % re.escape(title),
-            extract_text(find_main_content(browser.contents)))
+            extract_text(find_main_content(browser.contents)),
+        )
         self.assertEqual(self.build_url, browser.getLink(title).url)
 
     def makeBuildingRecipe(self, information_type=InformationType.PUBLIC):
         builder = self.factory.makeBuilder()
         build = self.factory.makeCharmRecipeBuild(
-            information_type=information_type)
+            information_type=information_type
+        )
         build.updateStatus(BuildStatus.BUILDING, builder=builder)
         build.queueBuild()
         build.buildqueue_record.builder = builder
diff --git a/lib/lp/charms/browser/tests/test_charmrecipelisting.py b/lib/lp/charms/browser/tests/test_charmrecipelisting.py
index ceba165..cf94126 100644
--- a/lib/lp/charms/browser/tests/test_charmrecipelisting.py
+++ b/lib/lp/charms/browser/tests/test_charmrecipelisting.py
@@ -3,26 +3,17 @@
 
 """Test charm recipe listings."""
 
-from datetime import (
-    datetime,
-    timedelta,
-    )
+from datetime import datetime, timedelta
 from functools import partial
 
 import pytz
 import soupmatchers
-from testtools.matchers import (
-    MatchesAll,
-    Not,
-    )
+from testtools.matchers import MatchesAll, Not
 from zope.security.proxy import removeSecurityProxy
 
 from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
 from lp.code.tests.helpers import GitHostingFixture
-from lp.services.database.constants import (
-    ONE_DAY_AGO,
-    UTC_NOW,
-    )
+from lp.services.database.constants import ONE_DAY_AGO, UTC_NOW
 from lp.services.features.testing import MemoryFeatureFixture
 from lp.services.webapp import canonical_url
 from lp.testing import (
@@ -31,7 +22,7 @@ from lp.testing import (
     login,
     person_logged_in,
     record_two_runs,
-    )
+)
 from lp.testing.layers import LaunchpadFunctionalLayer
 from lp.testing.matchers import HasQueryCount
 from lp.testing.views import create_initialized_view
@@ -41,16 +32,21 @@ class TestCharmRecipeListing(BrowserTestCase):
 
     layer = LaunchpadFunctionalLayer
 
-    def assertCharmRecipesLink(self, context, link_text,
-                               link_has_context=False, **kwargs):
+    def assertCharmRecipesLink(
+        self, context, link_text, link_has_context=False, **kwargs
+    ):
         if link_has_context:
             expected_href = canonical_url(context, view_name="+charm-recipes")
         else:
             expected_href = "+charm-recipes"
         matcher = soupmatchers.HTMLContains(
             soupmatchers.Tag(
-                "View charm recipes link", "a", text=link_text,
-                attrs={"href": expected_href}))
+                "View charm recipes link",
+                "a",
+                text=link_text,
+                attrs={"href": expected_href},
+            )
+        )
         self.assertThat(self.getViewBrowser(context).contents, Not(matcher))
         login(ANONYMOUS)
         with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
@@ -71,14 +67,19 @@ class TestCharmRecipeListing(BrowserTestCase):
     def test_person_links_to_recipes(self):
         person = self.factory.makePerson()
         self.assertCharmRecipesLink(
-            person, "View charm recipes", link_has_context=True,
-            registrant=person, owner=person)
+            person,
+            "View charm recipes",
+            link_has_context=True,
+            registrant=person,
+            owner=person,
+        )
 
     def test_project_links_to_recipes(self):
         project = self.factory.makeProduct()
         [ref] = self.factory.makeGitRefs(target=project)
         self.assertCharmRecipesLink(
-            project, "View charm recipes", link_has_context=True, git_ref=ref)
+            project, "View charm recipes", link_has_context=True, git_ref=ref
+        )
 
     def test_git_repository_recipe_listing(self):
         # We can see charm recipes for a Git repository.
@@ -87,10 +88,13 @@ class TestCharmRecipeListing(BrowserTestCase):
         with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
             self.factory.makeCharmRecipe(git_ref=ref)
         text = self.getMainText(repository, "+charm-recipes")
-        self.assertTextMatchesExpressionIgnoreWhitespace("""
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            """
             Charm recipes for lp:~.*
             Name            Owner           Registered
-            charm-name.*    Team Name.*     .*""", text)
+            charm-name.*    Team Name.*     .*""",
+            text,
+        )
 
     def test_git_ref_recipe_listing(self):
         # We can see charm recipes for a Git reference.
@@ -98,10 +102,13 @@ class TestCharmRecipeListing(BrowserTestCase):
         with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
             self.factory.makeCharmRecipe(git_ref=ref)
         text = self.getMainText(ref, "+charm-recipes")
-        self.assertTextMatchesExpressionIgnoreWhitespace("""
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            """
             Charm recipes for ~.*:.*
             Name            Owner           Registered
-            charm-name.*    Team Name.*     .*""", text)
+            charm-name.*    Team Name.*     .*""",
+            text,
+        )
 
     def test_person_recipe_listing(self):
         # We can see charm recipes for a person.
@@ -109,13 +116,19 @@ class TestCharmRecipeListing(BrowserTestCase):
         [ref] = self.factory.makeGitRefs()
         with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
             self.factory.makeCharmRecipe(
-                registrant=owner, owner=owner, git_ref=ref,
-                date_created=ONE_DAY_AGO)
+                registrant=owner,
+                owner=owner,
+                git_ref=ref,
+                date_created=ONE_DAY_AGO,
+            )
         text = self.getMainText(owner, "+charm-recipes")
-        self.assertTextMatchesExpressionIgnoreWhitespace("""
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            """
             Charm recipes for Charm Owner
             Name            Source                  Registered
-            charm-name.*    ~.*:.*                  .*""", text)
+            charm-name.*    ~.*:.*                  .*""",
+            text,
+        )
 
     def test_project_recipe_listing(self):
         # We can see charm recipes for a project.
@@ -124,16 +137,21 @@ class TestCharmRecipeListing(BrowserTestCase):
         with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
             self.factory.makeCharmRecipe(git_ref=ref, date_created=UTC_NOW)
         text = self.getMainText(project, "+charm-recipes")
-        self.assertTextMatchesExpressionIgnoreWhitespace("""
+        self.assertTextMatchesExpressionIgnoreWhitespace(
+            """
             Charm recipes for Charmable
             Name            Owner           Source          Registered
-            charm-name.*    Team Name.*     ~.*:.*          .*""", text)
+            charm-name.*    Team Name.*     ~.*:.*          .*""",
+            text,
+        )
 
     def assertCharmRecipesQueryCount(self, context, item_creator):
         self.pushConfig("launchpad", default_batch_size=10)
         recorder1, recorder2 = record_two_runs(
             lambda: self.getMainText(context, "+charm-recipes"),
-            item_creator, 5)
+            item_creator,
+            5,
+        )
         self.assertThat(recorder2, HasQueryCount.byEquality(recorder1))
 
     def test_git_repository_query_count(self):
@@ -199,34 +217,48 @@ class TestCharmRecipeListing(BrowserTestCase):
         with MemoryFeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}):
             recipes = [create_recipe() for i in range(count)]
         for i, recipe in enumerate(recipes):
-            removeSecurityProxy(recipe).date_last_modified = (
-                start_time - timedelta(seconds=i))
+            removeSecurityProxy(
+                recipe
+            ).date_last_modified = start_time - timedelta(seconds=i)
         return [
             soupmatchers.Tag(
-                "charm recipe link", "a", text=recipe.name,
+                "charm recipe link",
+                "a",
+                text=recipe.name,
                 attrs={
-                    "href": canonical_url(recipe, path_only_if_possible=True)})
-            for recipe in recipes]
+                    "href": canonical_url(recipe, path_only_if_possible=True)
+                },
+            )
+            for recipe in recipes
+        ]
 
     def assertBatches(self, context, link_matchers, batched, start, size):
         view = create_initialized_view(context, "+charm-recipes")
         listing_tag = soupmatchers.Tag(
-            "charm recipe listing", "table",
-            attrs={"class": "listing sortable"})
+            "charm recipe listing",
+            "table",
+            attrs={"class": "listing sortable"},
+        )
         batch_nav_tag = soupmatchers.Tag(
-            "batch nav links", "td",
-            attrs={"class": "batch-navigation-links"})
+            "batch nav links", "td", attrs={"class": "batch-navigation-links"}
+        )
         present_links = ([batch_nav_tag] if batched else []) + [
-            matcher for i, matcher in enumerate(link_matchers)
-            if i in range(start, start + size)]
+            matcher
+            for i, matcher in enumerate(link_matchers)
+            if i in range(start, start + size)
+        ]
         absent_links = ([] if batched else [batch_nav_tag]) + [
-            matcher for i, matcher in enumerate(link_matchers)
-            if i not in range(start, start + size)]
+            matcher
+            for i, matcher in enumerate(link_matchers)
+            if i not in range(start, start + size)
+        ]
         self.assertThat(
             view.render(),
             MatchesAll(
                 soupmatchers.HTMLContains(listing_tag, *present_links),
-                Not(soupmatchers.HTMLContains(*absent_links))))
+                Not(soupmatchers.HTMLContains(*absent_links)),
+            ),
+        )
 
     def test_git_repository_batches_recipes(self):
         repository = self.factory.makeGitRepository()
@@ -235,8 +267,11 @@ class TestCharmRecipeListing(BrowserTestCase):
         now = datetime.now(pytz.UTC)
         link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
         self.assertBatches(repository, link_matchers, False, 0, 3)
-        link_matchers.extend(self.makeCharmRecipesAndMatchers(
-            create_recipe, 7, now - timedelta(seconds=3)))
+        link_matchers.extend(
+            self.makeCharmRecipesAndMatchers(
+                create_recipe, 7, now - timedelta(seconds=3)
+            )
+        )
         self.assertBatches(repository, link_matchers, True, 0, 5)
 
     def test_git_ref_batches_recipes(self):
@@ -245,19 +280,26 @@ class TestCharmRecipeListing(BrowserTestCase):
         now = datetime.now(pytz.UTC)
         link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
         self.assertBatches(ref, link_matchers, False, 0, 3)
-        link_matchers.extend(self.makeCharmRecipesAndMatchers(
-            create_recipe, 7, now - timedelta(seconds=3)))
+        link_matchers.extend(
+            self.makeCharmRecipesAndMatchers(
+                create_recipe, 7, now - timedelta(seconds=3)
+            )
+        )
         self.assertBatches(ref, link_matchers, True, 0, 5)
 
     def test_person_batches_recipes(self):
         owner = self.factory.makePerson()
         create_recipe = partial(
-            self.factory.makeCharmRecipe, registrant=owner, owner=owner)
+            self.factory.makeCharmRecipe, registrant=owner, owner=owner
+        )
         now = datetime.now(pytz.UTC)
         link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
         self.assertBatches(owner, link_matchers, False, 0, 3)
-        link_matchers.extend(self.makeCharmRecipesAndMatchers(
-            create_recipe, 7, now - timedelta(seconds=3)))
+        link_matchers.extend(
+            self.makeCharmRecipesAndMatchers(
+                create_recipe, 7, now - timedelta(seconds=3)
+            )
+        )
         self.assertBatches(owner, link_matchers, True, 0, 5)
 
     def test_project_batches_recipes(self):
@@ -267,6 +309,9 @@ class TestCharmRecipeListing(BrowserTestCase):
         now = datetime.now(pytz.UTC)
         link_matchers = self.makeCharmRecipesAndMatchers(create_recipe, 3, now)
         self.assertBatches(project, link_matchers, False, 0, 3)
-        link_matchers.extend(self.makeCharmRecipesAndMatchers(
-            create_recipe, 7, now - timedelta(seconds=3)))
+        link_matchers.extend(
+            self.makeCharmRecipesAndMatchers(
+                create_recipe, 7, now - timedelta(seconds=3)
+            )
+        )
         self.assertBatches(project, link_matchers, True, 0, 5)
diff --git a/lib/lp/charms/browser/tests/test_hascharmrecipes.py b/lib/lp/charms/browser/tests/test_hascharmrecipes.py
index e114153..19adca7 100644
--- a/lib/lp/charms/browser/tests/test_hascharmrecipes.py
+++ b/lib/lp/charms/browser/tests/test_hascharmrecipes.py
@@ -4,10 +4,7 @@
 """Test views for objects that have charm recipes."""
 
 import soupmatchers
-from testscenarios import (
-    load_tests_apply_scenarios,
-    WithScenarios,
-    )
+from testscenarios import WithScenarios, load_tests_apply_scenarios
 from testtools.matchers import Not
 
 from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
@@ -33,15 +30,21 @@ class TestHasCharmRecipesView(WithScenarios, TestCaseWithFactory):
     layer = DatabaseFunctionalLayer
 
     scenarios = [
-        ("GitRepository", {
-            "context_type": "repository",
-            "context_factory": make_git_repository,
-            }),
-        ("GitRef", {
-            "context_type": "branch",
-            "context_factory": make_git_ref,
-            }),
-        ]
+        (
+            "GitRepository",
+            {
+                "context_type": "repository",
+                "context_factory": make_git_repository,
+            },
+        ),
+        (
+            "GitRef",
+            {
+                "context_type": "branch",
+                "context_factory": make_git_ref,
+            },
+        ),
+    ]
 
     def setUp(self):
         super().setUp()
@@ -58,16 +61,18 @@ class TestHasCharmRecipesView(WithScenarios, TestCaseWithFactory):
         view = create_initialized_view(context, "+index")
         self.assertEqual(
             "No charm recipes using this %s." % self.context_type,
-            view.charm_recipes_link)
+            view.charm_recipes_link,
+        )
 
     def test_charm_recipes_link_one_recipe(self):
         # An object with one charm recipe shows a link to that recipe.
         context = self.context_factory(self)
         recipe = self.makeCharmRecipe(context)
         view = create_initialized_view(context, "+index")
-        expected_link = (
-            '<a href="%s">1 charm recipe</a> using this %s.' %
-            (canonical_url(recipe), self.context_type))
+        expected_link = '<a href="%s">1 charm recipe</a> using this %s.' % (
+            canonical_url(recipe),
+            self.context_type,
+        )
         self.assertEqual(expected_link, view.charm_recipes_link)
 
     def test_charm_recipes_link_more_recipes(self):
@@ -77,8 +82,9 @@ class TestHasCharmRecipesView(WithScenarios, TestCaseWithFactory):
         self.makeCharmRecipe(context)
         view = create_initialized_view(context, "+index")
         expected_link = (
-            '<a href="+charm-recipes">2 charm recipes</a> using this %s.' %
-            self.context_type)
+            '<a href="+charm-recipes">2 charm recipes</a> using this %s.'
+            % self.context_type
+        )
         self.assertEqual(expected_link, view.charm_recipes_link)
 
 
@@ -88,7 +94,7 @@ class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory):
 
     scenarios = [
         ("GitRef", {"context_factory": make_git_ref}),
-        ]
+    ]
 
     def setUp(self):
         super().setUp()
@@ -103,11 +109,21 @@ class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory):
         context = self.context_factory(self)
         view = create_initialized_view(context, "+index")
         new_charm_recipe_url = canonical_url(
-            context, view_name="+new-charm-recipe")
-        self.assertThat(view(), Not(soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "creation link", "a", attrs={"href": new_charm_recipe_url},
-                text="Create charm recipe"))))
+            context, view_name="+new-charm-recipe"
+        )
+        self.assertThat(
+            view(),
+            Not(
+                soupmatchers.HTMLContains(
+                    soupmatchers.Tag(
+                        "creation link",
+                        "a",
+                        attrs={"href": new_charm_recipe_url},
+                        text="Create charm recipe",
+                    )
+                )
+            ),
+        )
 
     def test_creation_link_no_recipes(self):
         # An object with no charm recipes shows a creation link.
@@ -115,11 +131,19 @@ class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory):
         context = self.context_factory(self)
         view = create_initialized_view(context, "+index")
         new_charm_recipe_url = canonical_url(
-            context, view_name="+new-charm-recipe")
-        self.assertThat(view(), soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "creation link", "a", attrs={"href": new_charm_recipe_url},
-                text="Create charm recipe")))
+            context, view_name="+new-charm-recipe"
+        )
+        self.assertThat(
+            view(),
+            soupmatchers.HTMLContains(
+                soupmatchers.Tag(
+                    "creation link",
+                    "a",
+                    attrs={"href": new_charm_recipe_url},
+                    text="Create charm recipe",
+                )
+            ),
+        )
 
     def test_creation_link_recipes(self):
         # An object with charm recipes shows a creation link.
@@ -128,11 +152,19 @@ class TestHasCharmRecipesMenu(WithScenarios, TestCaseWithFactory):
         self.makeCharmRecipe(context)
         view = create_initialized_view(context, "+index")
         new_charm_recipe_url = canonical_url(
-            context, view_name="+new-charm-recipe")
-        self.assertThat(view(), soupmatchers.HTMLContains(
-            soupmatchers.Tag(
-                "creation link", "a", attrs={"href": new_charm_recipe_url},
-                text="Create charm recipe")))
+            context, view_name="+new-charm-recipe"
+        )
+        self.assertThat(
+            view(),
+            soupmatchers.HTMLContains(
+                soupmatchers.Tag(
+                    "creation link",
+                    "a",
+                    attrs={"href": new_charm_recipe_url},
+                    text="Create charm recipe",
+                )
+            ),
+        )
 
 
 load_tests = load_tests_apply_scenarios
diff --git a/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py b/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py
index 92af76e..5a15feb 100644
--- a/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py
+++ b/lib/lp/charms/browser/widgets/charmrecipebuildchannels.py
@@ -5,16 +5,12 @@
 
 __all__ = [
     "CharmRecipeBuildChannelsWidget",
-    ]
+]
 
 from zope.browserpage import ViewPageTemplateFile
 from zope.formlib.interfaces import IInputWidget
 from zope.formlib.utility import setUpWidget
-from zope.formlib.widget import (
-    BrowserWidget,
-    InputErrors,
-    InputWidget,
-    )
+from zope.formlib.widget import BrowserWidget, InputErrors, InputWidget
 from zope.interface import implementer
 from zope.schema import TextLine
 from zope.security.proxy import isinstance as zope_isinstance
@@ -23,7 +19,7 @@ from lp.app.errors import UnexpectedFormData
 from lp.services.webapp.interfaces import (
     IAlwaysSubmittedWidget,
     ISingleLineWidgetLayout,
-    )
+)
 
 
 @implementer(ISingleLineWidgetLayout, IAlwaysSubmittedWidget, IInputWidget)
@@ -38,20 +34,24 @@ class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget):
         super().__init__(context, request)
         self.hint = (
             "The channels to use for build tools when building the charm "
-            "recipe.")
+            "recipe."
+        )
 
     def setUpSubWidgets(self):
         if self._widgets_set_up:
             return
         fields = [
             TextLine(
-                __name__=snap_name, title="%s channel" % snap_name,
-                required=False)
+                __name__=snap_name,
+                title="%s channel" % snap_name,
+                required=False,
+            )
             for snap_name in self.snap_names
-            ]
+        ]
         for field in fields:
             setUpWidget(
-                self, field.__name__, field, IInputWidget, prefix=self.name)
+                self, field.__name__, field, IInputWidget, prefix=self.name
+            )
         self._widgets_set_up = True
 
     def setRenderedValue(self, value):
@@ -61,13 +61,15 @@ class CharmRecipeBuildChannelsWidget(BrowserWidget, InputWidget):
             value = {}
         for snap_name in self.snap_names:
             getattr(self, "%s_widget" % snap_name).setRenderedValue(
-                value.get(snap_name))
+                value.get(snap_name)
+            )
 
     def hasInput(self):
         """See `IInputWidget`."""
         return any(
             "%s.%s" % (self.name, snap_name) in self.request.form
-            for snap_name in self.snap_names)
+            for snap_name in self.snap_names
+        )
 
     def hasValidInput(self):
         """See `IInputWidget`."""
diff --git a/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py b/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py
index a5c922b..ab4a44c 100644
--- a/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py
+++ b/lib/lp/charms/browser/widgets/tests/test_charmrecipebuildchannelswidget.py
@@ -3,23 +3,17 @@
 
 import re
 
-from zope.formlib.interfaces import (
-    IBrowserWidget,
-    IInputWidget,
-    )
+from zope.formlib.interfaces import IBrowserWidget, IInputWidget
 from zope.schema import Dict
 
 from lp.charms.browser.widgets.charmrecipebuildchannels import (
     CharmRecipeBuildChannelsWidget,
-    )
+)
 from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
 from lp.services.beautifulsoup import BeautifulSoup
 from lp.services.features.testing import FeatureFixture
 from lp.services.webapp.servers import LaunchpadTestRequest
-from lp.testing import (
-    TestCaseWithFactory,
-    verifyObject,
-    )
+from lp.testing import TestCaseWithFactory, verifyObject
 from lp.testing.layers import DatabaseFunctionalLayer
 
 
@@ -32,7 +26,8 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
         self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
         field = Dict(
             __name__="auto_build_channels",
-            title="Source snap channels for automatic builds")
+            title="Source snap channels for automatic builds",
+        )
         self.context = self.factory.makeCharmRecipe()
         self.field = field.bind(self.context)
         self.request = LaunchpadTestRequest()
@@ -45,14 +40,17 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
     def test_template(self):
         self.assertTrue(
             self.widget.template.filename.endswith(
-                "charmrecipebuildchannels.pt"),
-            "Template was not set up.")
+                "charmrecipebuildchannels.pt"
+            ),
+            "Template was not set up.",
+        )
 
     def test_hint(self):
         self.assertEqual(
             "The channels to use for build tools when building the charm "
             "recipe.",
-            self.widget.hint)
+            self.widget.hint,
+        )
 
     def test_setUpSubWidgets_first_call(self):
         # The subwidgets are set up and a flag is set.
@@ -94,7 +92,8 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
     def test_setRenderedValue_one_channel(self):
         self.widget.setRenderedValue({"charmcraft": "stable"})
         self.assertEqual(
-            "stable", self.widget.charmcraft_widget._getCurrentValue())
+            "stable", self.widget.charmcraft_widget._getCurrentValue()
+        )
         self.assertIsNone(self.widget.core_widget._getCurrentValue())
         self.assertIsNone(self.widget.core18_widget._getCurrentValue())
         self.assertIsNone(self.widget.core20_widget._getCurrentValue())
@@ -102,16 +101,25 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
 
     def test_setRenderedValue_all_channels(self):
         self.widget.setRenderedValue(
-            {"charmcraft": "stable", "core": "candidate", "core18": "beta",
-             "core20": "edge", "core22": "edge/feature"})
+            {
+                "charmcraft": "stable",
+                "core": "candidate",
+                "core18": "beta",
+                "core20": "edge",
+                "core22": "edge/feature",
+            }
+        )
         self.assertEqual(
-            "stable", self.widget.charmcraft_widget._getCurrentValue())
+            "stable", self.widget.charmcraft_widget._getCurrentValue()
+        )
         self.assertEqual(
-            "candidate", self.widget.core_widget._getCurrentValue())
+            "candidate", self.widget.core_widget._getCurrentValue()
+        )
         self.assertEqual("beta", self.widget.core18_widget._getCurrentValue())
         self.assertEqual("edge", self.widget.core20_widget._getCurrentValue())
         self.assertEqual(
-            "edge/feature", self.widget.core22_widget._getCurrentValue())
+            "edge/feature", self.widget.core22_widget._getCurrentValue()
+        )
 
     def test_hasInput_false(self):
         # hasInput is false when there are no channels in the form data.
@@ -121,7 +129,8 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
     def test_hasInput_true(self):
         # hasInput is true when there are channels in the form data.
         self.widget.request = LaunchpadTestRequest(
-            form={"field.auto_build_channels.charmcraft": "stable"})
+            form={"field.auto_build_channels.charmcraft": "stable"}
+        )
         self.assertTrue(self.widget.hasInput())
 
     def test_hasValidInput_true(self):
@@ -134,7 +143,7 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
             "field.auto_build_channels.core18": "beta",
             "field.auto_build_channels.core20": "edge",
             "field.auto_build_channels.core22": "edge/feature",
-            }
+        }
         self.widget.request = LaunchpadTestRequest(form=form)
         self.assertTrue(self.widget.hasValidInput())
 
@@ -145,12 +154,17 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
             "field.auto_build_channels.core18": "beta",
             "field.auto_build_channels.core20": "edge",
             "field.auto_build_channels.core22": "edge/feature",
-            }
+        }
         self.widget.request = LaunchpadTestRequest(form=form)
         self.assertEqual(
-            {"charmcraft": "stable", "core18": "beta", "core20": "edge",
-             "core22": "edge/feature"},
-            self.widget.getInputValue())
+            {
+                "charmcraft": "stable",
+                "core18": "beta",
+                "core20": "edge",
+                "core22": "edge/feature",
+            },
+            self.widget.getInputValue(),
+        )
 
     def test_call(self):
         # The __call__ method sets up the widgets.
@@ -168,6 +182,6 @@ class TestCharmRecipeBuildChannelsWidget(TestCaseWithFactory):
             "field.auto_build_channels.core18",
             "field.auto_build_channels.core20",
             "field.auto_build_channels.core22",
-            ]
+        ]
         ids = [field["id"] for field in fields]
         self.assertContentEqual(expected_ids, ids)
diff --git a/lib/lp/charms/interfaces/charmbase.py b/lib/lp/charms/interfaces/charmbase.py
index f22c521..a7766fb 100644
--- a/lib/lp/charms/interfaces/charmbase.py
+++ b/lib/lp/charms/interfaces/charmbase.py
@@ -8,11 +8,12 @@ __all__ = [
     "ICharmBase",
     "ICharmBaseSet",
     "NoSuchCharmBase",
-    ]
+]
 
 import http.client
 
 from lazr.restful.declarations import (
+    REQUEST_USER,
     call_with,
     collection_default_content,
     error_status,
@@ -26,20 +27,10 @@ from lazr.restful.declarations import (
     operation_for_version,
     operation_parameters,
     operation_returns_entry,
-    REQUEST_USER,
-    )
-from lazr.restful.fields import (
-    CollectionField,
-    Reference,
-    )
+)
+from lazr.restful.fields import CollectionField, Reference
 from zope.interface import Interface
-from zope.schema import (
-    Datetime,
-    Dict,
-    Int,
-    List,
-    TextLine,
-    )
+from zope.schema import Datetime, Dict, Int, List, TextLine
 
 from lp import _
 from lp.app.errors import NotFoundError
@@ -54,7 +45,8 @@ class DuplicateCharmBase(Exception):
 
     def __init__(self, distro_series):
         super().__init__(
-            "%s is already in use by another base." % distro_series)
+            "%s is already in use by another base." % distro_series
+        )
 
 
 class NoSuchCharmBase(NotFoundError):
@@ -73,23 +65,37 @@ class ICharmBaseView(Interface):
 
     id = Int(title=_("ID"), required=True, readonly=True)
 
-    date_created = exported(Datetime(
-        title=_("Date created"), required=True, readonly=True))
+    date_created = exported(
+        Datetime(title=_("Date created"), required=True, readonly=True)
+    )
 
-    registrant = exported(PublicPersonChoice(
-        title=_("Registrant"), required=True, readonly=True,
-        vocabulary="ValidPersonOrTeam",
-        description=_("The person who registered this base.")))
+    registrant = exported(
+        PublicPersonChoice(
+            title=_("Registrant"),
+            required=True,
+            readonly=True,
+            vocabulary="ValidPersonOrTeam",
+            description=_("The person who registered this base."),
+        )
+    )
 
-    distro_series = exported(Reference(
-        IDistroSeries, title=_("Distro series"),
-        required=True, readonly=True))
+    distro_series = exported(
+        Reference(
+            IDistroSeries,
+            title=_("Distro series"),
+            required=True,
+            readonly=True,
+        )
+    )
 
-    processors = exported(CollectionField(
-        title=_("Processors"),
-        description=_("The architectures that the charm base supports."),
-        value_type=Reference(schema=IProcessor),
-        readonly=True))
+    processors = exported(
+        CollectionField(
+            title=_("Processors"),
+            description=_("The architectures that the charm base supports."),
+            value_type=Reference(schema=IProcessor),
+            readonly=True,
+        )
+    )
 
 
 class ICharmBaseEditableAttributes(Interface):
@@ -98,24 +104,30 @@ class ICharmBaseEditableAttributes(Interface):
     Anyone can view these attributes, but they need launchpad.Edit to change.
     """
 
-    build_snap_channels = exported(Dict(
-        title=_("Source snap channels for builds"),
-        key_type=TextLine(), required=True, readonly=False,
-        description=_(
-            "A dictionary mapping snap names to channels to use when building "
-            "charm recipes that specify this base.  The special '_byarch' key "
-            "may have a mapping of architecture names to mappings of snap "
-            "names to channels, which if present override the channels "
-            "declared at the top level when building for those "
-            "architectures.")))
+    build_snap_channels = exported(
+        Dict(
+            title=_("Source snap channels for builds"),
+            key_type=TextLine(),
+            required=True,
+            readonly=False,
+            description=_(
+                "A dictionary mapping snap names to channels to use when "
+                "building charm recipes that specify this base.  The special "
+                "'_byarch' key may have a mapping of architecture names to "
+                "mappings of snap names to channels, which if present "
+                "override the channels declared at the top level when "
+                "building for those architectures."
+            ),
+        )
+    )
 
 
 class ICharmBaseEdit(Interface):
     """`ICharmBase` methods that require launchpad.Edit permission."""
 
     @operation_parameters(
-        processors=List(
-            value_type=Reference(schema=IProcessor), required=True))
+        processors=List(value_type=Reference(schema=IProcessor), required=True)
+    )
     @export_write_operation()
     @operation_for_version("devel")
     def setProcessors(processors):
@@ -141,12 +153,20 @@ class ICharmBaseSetEdit(Interface):
     @call_with(registrant=REQUEST_USER)
     @operation_parameters(
         processors=List(
-            value_type=Reference(schema=IProcessor), required=False))
+            value_type=Reference(schema=IProcessor), required=False
+        )
+    )
     @export_factory_operation(
-        ICharmBase, ["distro_series", "build_snap_channels"])
+        ICharmBase, ["distro_series", "build_snap_channels"]
+    )
     @operation_for_version("devel")
-    def new(registrant, distro_series, build_snap_channels, processors=None,
-            date_created=None):
+    def new(
+        registrant,
+        distro_series,
+        build_snap_channels,
+        processors=None,
+        date_created=None,
+    ):
         """Create an `ICharmBase`."""
 
 
@@ -162,7 +182,9 @@ class ICharmBaseSet(ICharmBaseSetEdit):
 
     @operation_parameters(
         distro_series=Reference(
-            schema=IDistroSeries, title=_("Distro series"), required=True))
+            schema=IDistroSeries, title=_("Distro series"), required=True
+        )
+    )
     @operation_returns_entry(ICharmBase)
     @export_read_operation()
     @operation_for_version("devel")
diff --git a/lib/lp/charms/interfaces/charmhubclient.py b/lib/lp/charms/interfaces/charmhubclient.py
index 9a7cc0f..c70b39d 100644
--- a/lib/lp/charms/interfaces/charmhubclient.py
+++ b/lib/lp/charms/interfaces/charmhubclient.py
@@ -14,7 +14,7 @@ __all__ = [
     "UnauthorizedUploadResponse",
     "UploadFailedResponse",
     "UploadNotReviewedYetResponse",
-    ]
+]
 
 import http.client
 
@@ -23,7 +23,6 @@ from zope.interface import Interface
 
 
 class CharmhubError(Exception):
-
     def __init__(self, message="", detail=None, can_retry=False):
         super().__init__(message)
         self.message = message
diff --git a/lib/lp/charms/interfaces/charmrecipe.py b/lib/lp/charms/interfaces/charmrecipe.py
index 50d37ec..35c60e2 100644
--- a/lib/lp/charms/interfaces/charmrecipe.py
+++ b/lib/lp/charms/interfaces/charmrecipe.py
@@ -28,16 +28,14 @@ __all__ = [
     "MissingCharmcraftYaml",
     "NoSourceForCharmRecipe",
     "NoSuchCharmRecipe",
-    ]
+]
 
 import http.client
 
-from lazr.enum import (
-    EnumeratedType,
-    Item,
-    )
+from lazr.enum import EnumeratedType, Item
 from lazr.lifecycle.snapshot import doNotSnapshot
 from lazr.restful.declarations import (
+    REQUEST_USER,
     call_with,
     collection_default_content,
     error_status,
@@ -53,18 +51,10 @@ from lazr.restful.declarations import (
     operation_returns_collection_of,
     operation_returns_entry,
     rename_parameters_as,
-    REQUEST_USER,
-    )
-from lazr.restful.fields import (
-    CollectionField,
-    Reference,
-    ReferenceChoice,
-    )
+)
+from lazr.restful.fields import CollectionField, Reference, ReferenceChoice
 from lazr.restful.interface import copy_field
-from zope.interface import (
-    Attribute,
-    Interface,
-    )
+from zope.interface import Attribute, Interface
 from zope.schema import (
     Bool,
     Choice,
@@ -75,7 +65,7 @@ from zope.schema import (
     Set,
     Text,
     TextLine,
-    )
+)
 from zope.security.interfaces import Unauthorized
 
 from lp import _
@@ -89,14 +79,10 @@ from lp.code.interfaces.gitref import IGitRef
 from lp.code.interfaces.gitrepository import IGitRepository
 from lp.registry.interfaces.person import IPerson
 from lp.registry.interfaces.product import IProduct
-from lp.services.fields import (
-    PersonChoice,
-    PublicPersonChoice,
-    )
+from lp.services.fields import PersonChoice, PublicPersonChoice
 from lp.services.webhooks.interfaces import IWebhookTarget
 from lp.snappy.validators.channels import channels_validator
 
-
 CHARM_RECIPE_ALLOW_CREATE = "charm.recipe.create.enabled"
 CHARM_RECIPE_PRIVATE_FEATURE_FLAG = "charm.recipe.allow_private"
 CHARM_RECIPE_BUILD_DISTRIBUTION = "charm.default_build_distribution"
@@ -109,7 +95,8 @@ class CharmRecipeFeatureDisabled(Unauthorized):
 
     def __init__(self):
         super().__init__(
-            "You do not have permission to create new charm recipes.")
+            "You do not have permission to create new charm recipes."
+        )
 
 
 @error_status(http.client.UNAUTHORIZED)
@@ -118,7 +105,8 @@ class CharmRecipePrivateFeatureDisabled(Unauthorized):
 
     def __init__(self):
         super().__init__(
-            "You do not have permission to create private charm recipes.")
+            "You do not have permission to create private charm recipes."
+        )
 
 
 @error_status(http.client.BAD_REQUEST)
@@ -128,7 +116,8 @@ class DuplicateCharmRecipeName(Exception):
     def __init__(self):
         super().__init__(
             "There is already a charm recipe with the same project, owner, "
-            "and name.")
+            "and name."
+        )
 
 
 @error_status(http.client.UNAUTHORIZED)
@@ -138,6 +127,7 @@ class CharmRecipeNotOwner(Unauthorized):
 
 class NoSuchCharmRecipe(NameLookupFailed):
     """The requested charm recipe does not exist."""
+
     _message_prefix = "No such charm recipe with this owner and project"
 
 
@@ -159,9 +149,12 @@ class CharmRecipePrivacyMismatch(Exception):
     """Charm recipe privacy does not match its content."""
 
     def __init__(self, message=None):
-        super().__init__(
-            message or
-            "Charm recipe contains private information and cannot be public.")
+        if message is None:
+            message = (
+                "Charm recipe contains private information and cannot be "
+                "public."
+            )
+        super().__init__(message)
 
 
 class BadCharmRecipeSearchContext(Exception):
@@ -194,7 +187,8 @@ class CharmRecipeBuildAlreadyPending(Exception):
 
     def __init__(self):
         super().__init__(
-            "An identical build of this charm recipe is already pending.")
+            "An identical build of this charm recipe is already pending."
+        )
 
 
 @error_status(http.client.BAD_REQUEST)
@@ -203,30 +197,37 @@ class CharmRecipeBuildDisallowedArchitecture(Exception):
 
     def __init__(self, das):
         super().__init__(
-            "This charm recipe is not allowed to build for %s/%s." %
-            (das.distroseries.name, das.architecturetag))
+            "This charm recipe is not allowed to build for %s/%s."
+            % (das.distroseries.name, das.architecturetag)
+        )
 
 
 class CharmRecipeBuildRequestStatus(EnumeratedType):
     """The status of a request to build a charm recipe."""
 
-    PENDING = Item("""
+    PENDING = Item(
+        """
         Pending
 
         This charm recipe build request is pending.
-        """)
+        """
+    )
 
-    FAILED = Item("""
+    FAILED = Item(
+        """
         Failed
 
         This charm recipe build request failed.
-        """)
+        """
+    )
 
-    COMPLETED = Item("""
+    COMPLETED = Item(
+        """
         Completed
 
         This charm recipe build request completed successfully.
-        """)
+        """
+    )
 
 
 # XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL
@@ -238,43 +239,76 @@ class ICharmRecipeBuildRequest(Interface):
 
     id = Int(title=_("ID"), required=True, readonly=True)
 
-    date_requested = exported(Datetime(
-        title=_("The time when this request was made"),
-        required=True, readonly=True))
+    date_requested = exported(
+        Datetime(
+            title=_("The time when this request was made"),
+            required=True,
+            readonly=True,
+        )
+    )
 
-    date_finished = exported(Datetime(
-        title=_("The time when this request finished"),
-        required=False, readonly=True))
+    date_finished = exported(
+        Datetime(
+            title=_("The time when this request finished"),
+            required=False,
+            readonly=True,
+        )
+    )
 
-    recipe = exported(Reference(
-        # Really ICharmRecipe, patched in lp.charms.interfaces.webservice.
-        Interface,
-        title=_("Charm recipe"), required=True, readonly=True))
+    recipe = exported(
+        Reference(
+            # Really ICharmRecipe, patched in lp.charms.interfaces.webservice.
+            Interface,
+            title=_("Charm recipe"),
+            required=True,
+            readonly=True,
+        )
+    )
 
-    status = exported(Choice(
-        title=_("Status"), vocabulary=CharmRecipeBuildRequestStatus,
-        required=True, readonly=True))
+    status = exported(
+        Choice(
+            title=_("Status"),
+            vocabulary=CharmRecipeBuildRequestStatus,
+            required=True,
+            readonly=True,
+        )
+    )
 
-    error_message = exported(TextLine(
-        title=_("Error message"), required=True, readonly=True))
+    error_message = exported(
+        TextLine(title=_("Error message"), required=True, readonly=True)
+    )
 
-    builds = exported(CollectionField(
-        title=_("Builds produced by this request"),
-        # Really ICharmRecipeBuild, patched in lp.charms.interfaces.webservice.
-        value_type=Reference(schema=Interface),
-        required=True, readonly=True))
+    builds = exported(
+        CollectionField(
+            title=_("Builds produced by this request"),
+            # Really ICharmRecipeBuild, patched in
+            # lp.charms.interfaces.webservice.
+            value_type=Reference(schema=Interface),
+            required=True,
+            readonly=True,
+        )
+    )
 
     requester = Reference(
-        title=_("The person requesting the builds."), schema=IPerson,
-        required=True, readonly=True)
+        title=_("The person requesting the builds."),
+        schema=IPerson,
+        required=True,
+        readonly=True,
+    )
 
     channels = Dict(
         title=_("Source snap channels for builds produced by this request"),
-        key_type=TextLine(), required=False, readonly=True)
+        key_type=TextLine(),
+        required=False,
+        readonly=True,
+    )
 
     architectures = Set(
         title=_("If set, this request is limited to these architecture tags"),
-        value_type=TextLine(), required=False, readonly=True)
+        value_type=TextLine(),
+        required=False,
+        readonly=True,
+    )
 
 
 class ICharmRecipeView(Interface):
@@ -282,28 +316,47 @@ class ICharmRecipeView(Interface):
 
     id = Int(title=_("ID"), required=True, readonly=True)
 
-    date_created = exported(Datetime(
-        title=_("Date created"), required=True, readonly=True))
-    date_last_modified = exported(Datetime(
-        title=_("Date last modified"), required=True, readonly=True))
+    date_created = exported(
+        Datetime(title=_("Date created"), required=True, readonly=True)
+    )
+    date_last_modified = exported(
+        Datetime(title=_("Date last modified"), required=True, readonly=True)
+    )
 
-    registrant = exported(PublicPersonChoice(
-        title=_("Registrant"), required=True, readonly=True,
-        vocabulary="ValidPersonOrTeam",
-        description=_("The person who registered this charm recipe.")))
+    registrant = exported(
+        PublicPersonChoice(
+            title=_("Registrant"),
+            required=True,
+            readonly=True,
+            vocabulary="ValidPersonOrTeam",
+            description=_("The person who registered this charm recipe."),
+        )
+    )
 
     source = Attribute(
-        "The source branch for this charm recipe (VCS-agnostic).")
+        "The source branch for this charm recipe (VCS-agnostic)."
+    )
 
-    private = exported(Bool(
-        title=_("Private"), required=False, readonly=False,
-        description=_("Whether this charm recipe is private.")))
+    private = exported(
+        Bool(
+            title=_("Private"),
+            required=False,
+            readonly=False,
+            description=_("Whether this charm recipe is private."),
+        )
+    )
 
-    can_upload_to_store = exported(Bool(
-        title=_("Can upload to Charmhub"), required=True, readonly=True,
-        description=_(
-            "Whether everything is set up to allow uploading builds of this "
-            "charm recipe to Charmhub.")))
+    can_upload_to_store = exported(
+        Bool(
+            title=_("Can upload to Charmhub"),
+            required=True,
+            readonly=True,
+            description=_(
+                "Whether everything is set up to allow uploading builds of "
+                "this charm recipe to Charmhub."
+            ),
+        )
+    )
 
     def getAllowedInformationTypes(user):
         """Get a list of acceptable `InformationType`s for this charm recipe.
@@ -314,8 +367,9 @@ class ICharmRecipeView(Interface):
     def visibleByUser(user):
         """Can the specified user see this charm recipe?"""
 
-    def requestBuild(build_request, distro_arch_series, charm_base=None,
-                     channels=None):
+    def requestBuild(
+        build_request, distro_arch_series, charm_base=None, channels=None
+    ):
         """Request a single build of this charm recipe.
 
         This method is for internal use; external callers should use
@@ -337,8 +391,12 @@ class ICharmRecipeView(Interface):
             description=_(
                 "A dictionary mapping snap names to channels to use for these "
                 "builds.  Currently only 'charmcraft', 'core', 'core18', "
-                "'core20', and 'core22' keys are supported."),
-            key_type=TextLine(), required=False))
+                "'core20', and 'core22' keys are supported."
+            ),
+            key_type=TextLine(),
+            required=False,
+        )
+    )
     @export_factory_operation(ICharmRecipeBuildRequest, [])
     @operation_for_version("devel")
     def requestBuilds(requester, channels=None, architectures=None):
@@ -357,8 +415,13 @@ class ICharmRecipeView(Interface):
         :return: An `ICharmRecipeBuildRequest`.
         """
 
-    def requestBuildsFromJob(build_request, channels=None, architectures=None,
-                             allow_failures=False, logger=None):
+    def requestBuildsFromJob(
+        build_request,
+        channels=None,
+        architectures=None,
+        allow_failures=False,
+        logger=None,
+    ):
         """Synchronous part of `CharmRecipe.requestBuilds`.
 
         Request that the charm recipe be built for relevant architectures.
@@ -391,39 +454,76 @@ class ICharmRecipeView(Interface):
         :return: `ICharmRecipeBuildRequest`.
         """
 
-    pending_build_requests = exported(doNotSnapshot(CollectionField(
-        title=_("Pending build requests for this charm recipe."),
-        value_type=Reference(ICharmRecipeBuildRequest),
-        required=True, readonly=True)))
+    pending_build_requests = exported(
+        doNotSnapshot(
+            CollectionField(
+                title=_("Pending build requests for this charm recipe."),
+                value_type=Reference(ICharmRecipeBuildRequest),
+                required=True,
+                readonly=True,
+            )
+        )
+    )
 
-    failed_build_requests = exported(doNotSnapshot(CollectionField(
-        title=_("Failed build requests for this charm recipe."),
-        value_type=Reference(ICharmRecipeBuildRequest),
-        required=True, readonly=True)))
+    failed_build_requests = exported(
+        doNotSnapshot(
+            CollectionField(
+                title=_("Failed build requests for this charm recipe."),
+                value_type=Reference(ICharmRecipeBuildRequest),
+                required=True,
+                readonly=True,
+            )
+        )
+    )
 
-    builds = exported(doNotSnapshot(CollectionField(
-        title=_("All builds of this charm recipe."),
-        description=_(
-            "All builds of this charm recipe, sorted in descending order "
-            "of finishing (or starting if not completed successfully)."),
-        # Really ICharmRecipeBuild, patched in lp.charms.interfaces.webservice.
-        value_type=Reference(schema=Interface), readonly=True)))
+    builds = exported(
+        doNotSnapshot(
+            CollectionField(
+                title=_("All builds of this charm recipe."),
+                description=_(
+                    "All builds of this charm recipe, sorted in descending "
+                    "order of finishing (or starting if not completed "
+                    "successfully)."
+                ),
+                # Really ICharmRecipeBuild, patched in
+                # lp.charms.interfaces.webservice.
+                value_type=Reference(schema=Interface),
+                readonly=True,
+            )
+        )
+    )
 
-    completed_builds = exported(doNotSnapshot(CollectionField(
-        title=_("Completed builds of this charm recipe."),
-        description=_(
-            "Completed builds of this charm recipe, sorted in descending "
-            "order of finishing."),
-        # Really ICharmRecipeBuild, patched in lp.charms.interfaces.webservice.
-        value_type=Reference(schema=Interface), readonly=True)))
+    completed_builds = exported(
+        doNotSnapshot(
+            CollectionField(
+                title=_("Completed builds of this charm recipe."),
+                description=_(
+                    "Completed builds of this charm recipe, sorted in "
+                    "descending order of finishing."
+                ),
+                # Really ICharmRecipeBuild, patched in
+                # lp.charms.interfaces.webservice.
+                value_type=Reference(schema=Interface),
+                readonly=True,
+            )
+        )
+    )
 
-    pending_builds = exported(doNotSnapshot(CollectionField(
-        title=_("Pending builds of this charm recipe."),
-        description=_(
-            "Pending builds of this charm recipe, sorted in descending "
-            "order of creation."),
-        # Really ICharmRecipeBuild, patched in lp.charms.interfaces.webservice.
-        value_type=Reference(schema=Interface), readonly=True)))
+    pending_builds = exported(
+        doNotSnapshot(
+            CollectionField(
+                title=_("Pending builds of this charm recipe."),
+                description=_(
+                    "Pending builds of this charm recipe, sorted in "
+                    "descending order of creation."
+                ),
+                # Really ICharmRecipeBuild, patched in
+                # lp.charms.interfaces.webservice.
+                value_type=Reference(schema=Interface),
+                readonly=True,
+            )
+        )
+    )
 
 
 class ICharmRecipeEdit(IWebhookTarget):
@@ -449,7 +549,9 @@ class ICharmRecipeEdit(IWebhookTarget):
     @rename_parameters_as(unbound_discharge_macaroon_raw="discharge_macaroon")
     @operation_parameters(
         unbound_discharge_macaroon_raw=TextLine(
-            title=_("Serialized discharge macaroon")))
+            title=_("Serialized discharge macaroon")
+        )
+    )
     @export_write_operation()
     @operation_for_version("devel")
     def completeAuthorization(unbound_discharge_macaroon_raw):
@@ -473,100 +575,179 @@ class ICharmRecipeEditableAttributes(Interface):
     These attributes need launchpad.View to see, and launchpad.Edit to change.
     """
 
-    owner = exported(PersonChoice(
-        title=_("Owner"), required=True, readonly=False,
-        vocabulary="AllUserTeamsParticipationPlusSelf",
-        description=_("The owner of this charm recipe.")))
+    owner = exported(
+        PersonChoice(
+            title=_("Owner"),
+            required=True,
+            readonly=False,
+            vocabulary="AllUserTeamsParticipationPlusSelf",
+            description=_("The owner of this charm recipe."),
+        )
+    )
 
-    project = exported(ReferenceChoice(
-        title=_("The project that this charm recipe is associated with"),
-        schema=IProduct, vocabulary="Product",
-        required=True, readonly=False))
+    project = exported(
+        ReferenceChoice(
+            title=_("The project that this charm recipe is associated with"),
+            schema=IProduct,
+            vocabulary="Product",
+            required=True,
+            readonly=False,
+        )
+    )
 
-    name = exported(TextLine(
-        title=_("Charm recipe name"), required=True, readonly=False,
-        constraint=name_validator,
-        description=_("The name of the charm recipe.")))
+    name = exported(
+        TextLine(
+            title=_("Charm recipe name"),
+            required=True,
+            readonly=False,
+            constraint=name_validator,
+            description=_("The name of the charm recipe."),
+        )
+    )
 
-    description = exported(Text(
-        title=_("Description"), required=False, readonly=False,
-        description=_("A description of the charm recipe.")))
+    description = exported(
+        Text(
+            title=_("Description"),
+            required=False,
+            readonly=False,
+            description=_("A description of the charm recipe."),
+        )
+    )
 
     git_repository = ReferenceChoice(
         title=_("Git repository"),
-        schema=IGitRepository, vocabulary="GitRepository",
-        required=False, readonly=True,
+        schema=IGitRepository,
+        vocabulary="GitRepository",
+        required=False,
+        readonly=True,
         description=_(
-            "A Git repository with a branch containing a charm recipe."))
+            "A Git repository with a branch containing a charm recipe."
+        ),
+    )
 
     git_path = TextLine(
-        title=_("Git branch path"), required=False, readonly=True,
-        description=_("The path of the Git branch containing a charm recipe."))
+        title=_("Git branch path"),
+        required=False,
+        readonly=True,
+        description=_("The path of the Git branch containing a charm recipe."),
+    )
 
-    git_ref = exported(Reference(
-        IGitRef, title=_("Git branch"), required=False, readonly=False,
-        description=_("The Git branch containing a charm recipe.")))
+    git_ref = exported(
+        Reference(
+            IGitRef,
+            title=_("Git branch"),
+            required=False,
+            readonly=False,
+            description=_("The Git branch containing a charm recipe."),
+        )
+    )
 
-    build_path = exported(TextLine(
-        title=_("Build path"),
-        description=_(
-            "Subdirectory within the branch containing metadata.yaml."),
-        constraint=path_does_not_escape, required=False, readonly=False))
+    build_path = exported(
+        TextLine(
+            title=_("Build path"),
+            description=_(
+                "Subdirectory within the branch containing metadata.yaml."
+            ),
+            constraint=path_does_not_escape,
+            required=False,
+            readonly=False,
+        )
+    )
 
-    information_type = exported(Choice(
-        title=_("Information type"), vocabulary=InformationType,
-        required=True, readonly=False, default=InformationType.PUBLIC,
-        description=_(
-            "The type of information contained in this charm recipe.")))
+    information_type = exported(
+        Choice(
+            title=_("Information type"),
+            vocabulary=InformationType,
+            required=True,
+            readonly=False,
+            default=InformationType.PUBLIC,
+            description=_(
+                "The type of information contained in this charm recipe."
+            ),
+        )
+    )
 
-    auto_build = exported(Bool(
-        title=_("Automatically build when branch changes"),
-        required=True, readonly=False,
-        description=_(
-            "Whether this charm recipe is built automatically when its branch "
-            "changes.")))
+    auto_build = exported(
+        Bool(
+            title=_("Automatically build when branch changes"),
+            required=True,
+            readonly=False,
+            description=_(
+                "Whether this charm recipe is built automatically when its "
+                "branch changes."
+            ),
+        )
+    )
 
-    auto_build_channels = exported(Dict(
-        title=_("Source snap channels for automatic builds"),
-        key_type=TextLine(), required=False, readonly=False,
-        description=_(
-            "A dictionary mapping snap names to channels to use when building "
-            "this charm recipe.  Currently only 'charmcraft', 'core', "
-            "'core18', 'core20', and 'core22' keys are supported.")))
+    auto_build_channels = exported(
+        Dict(
+            title=_("Source snap channels for automatic builds"),
+            key_type=TextLine(),
+            required=False,
+            readonly=False,
+            description=_(
+                "A dictionary mapping snap names to channels to use when "
+                "building this charm recipe.  Currently only 'charmcraft', "
+                "'core', 'core18', 'core20', and 'core22' keys are supported."
+            ),
+        )
+    )
 
-    is_stale = exported(Bool(
-        title=_("Charm recipe is stale and is due to be rebuilt."),
-        required=True, readonly=True))
+    is_stale = exported(
+        Bool(
+            title=_("Charm recipe is stale and is due to be rebuilt."),
+            required=True,
+            readonly=True,
+        )
+    )
 
-    store_upload = exported(Bool(
-        title=_("Automatically upload to store"),
-        required=True, readonly=False,
-        description=_(
-            "Whether builds of this charm recipe are automatically uploaded "
-            "to the store.")))
+    store_upload = exported(
+        Bool(
+            title=_("Automatically upload to store"),
+            required=True,
+            readonly=False,
+            description=_(
+                "Whether builds of this charm recipe are automatically "
+                "uploaded to the store."
+            ),
+        )
+    )
 
-    store_name = exported(TextLine(
-        title=_("Registered store name"),
-        required=False, readonly=False,
-        description=_(
-            "The registered name of this charm in the store.")))
+    store_name = exported(
+        TextLine(
+            title=_("Registered store name"),
+            required=False,
+            readonly=False,
+            description=_("The registered name of this charm in the store."),
+        )
+    )
 
     store_secrets = List(
-        value_type=TextLine(), title=_("Store upload tokens"),
-        required=False, readonly=False,
+        value_type=TextLine(),
+        title=_("Store upload tokens"),
+        required=False,
+        readonly=False,
         description=_(
             "Serialized secrets issued by the store and the login service to "
-            "authorize uploads of this charm recipe."))
+            "authorize uploads of this charm recipe."
+        ),
+    )
 
-    store_channels = exported(List(
-        title=_("Store channels"),
-        required=False, readonly=False, constraint=channels_validator,
-        description=_(
-            "Channels to release this charm to after uploading it to the "
-            "store. A channel is defined by a combination of an optional "
-            "track, a risk, and an optional branch, e.g. "
-            "'2.1/stable/fix-123', '2.1/stable', 'stable/fix-123', or "
-            "'stable'.")))
+    store_channels = exported(
+        List(
+            title=_("Store channels"),
+            required=False,
+            readonly=False,
+            constraint=channels_validator,
+            description=_(
+                "Channels to release this charm to after uploading it to the "
+                "store. A channel is defined by a combination of an optional "
+                "track, a risk, and an optional branch, e.g. "
+                "'2.1/stable/fix-123', '2.1/stable', 'stable/fix-123', or "
+                "'stable'."
+            ),
+        )
+    )
 
 
 class ICharmRecipeAdminAttributes(Interface):
@@ -575,9 +756,14 @@ class ICharmRecipeAdminAttributes(Interface):
     These attributes need launchpad.View to see, and launchpad.Admin to change.
     """
 
-    require_virtualized = exported(Bool(
-        title=_("Require virtualized builders"), required=True, readonly=False,
-        description=_("Only build this charm recipe on virtual builders.")))
+    require_virtualized = exported(
+        Bool(
+            title=_("Require virtualized builders"),
+            required=True,
+            readonly=False,
+            description=_("Only build this charm recipe on virtual builders."),
+        )
+    )
 
 
 # XXX cjwatson 2021-09-15 bug=760849: "beta" is a lie to get WADL
@@ -585,8 +771,13 @@ class ICharmRecipeAdminAttributes(Interface):
 # "devel".
 @exported_as_webservice_entry(as_of="beta")
 class ICharmRecipe(
-        ICharmRecipeView, ICharmRecipeEdit, ICharmRecipeEditableAttributes,
-        ICharmRecipeAdminAttributes, IPrivacy, IInformationType):
+    ICharmRecipeView,
+    ICharmRecipeEdit,
+    ICharmRecipeEditableAttributes,
+    ICharmRecipeAdminAttributes,
+    IPrivacy,
+    IInformationType,
+):
     """A buildable charm recipe."""
 
 
@@ -597,25 +788,51 @@ class ICharmRecipeSet(Interface):
     @call_with(registrant=REQUEST_USER)
     @operation_parameters(
         information_type=copy_field(
-            ICharmRecipe["information_type"], required=False))
+            ICharmRecipe["information_type"], required=False
+        )
+    )
     @export_factory_operation(
-        ICharmRecipe, [
-            "owner", "project", "name", "description", "git_ref", "build_path",
-            "auto_build", "auto_build_channels", "store_upload", "store_name",
+        ICharmRecipe,
+        [
+            "owner",
+            "project",
+            "name",
+            "description",
+            "git_ref",
+            "build_path",
+            "auto_build",
+            "auto_build_channels",
+            "store_upload",
+            "store_name",
             "store_channels",
-            ])
+        ],
+    )
     @operation_for_version("devel")
-    def new(registrant, owner, project, name, description=None, git_ref=None,
-            build_path=None, require_virtualized=True,
-            information_type=InformationType.PUBLIC, auto_build=False,
-            auto_build_channels=None, store_upload=False, store_name=None,
-            store_secrets=None, store_channels=None, date_created=None):
+    def new(
+        registrant,
+        owner,
+        project,
+        name,
+        description=None,
+        git_ref=None,
+        build_path=None,
+        require_virtualized=True,
+        information_type=InformationType.PUBLIC,
+        auto_build=False,
+        auto_build_channels=None,
+        store_upload=False,
+        store_name=None,
+        store_secrets=None,
+        store_channels=None,
+        date_created=None,
+    ):
         """Create an `ICharmRecipe`."""
 
     @operation_parameters(
         owner=Reference(IPerson, title=_("Owner"), required=True),
         project=Reference(IProduct, title=_("Project"), required=True),
-        name=TextLine(title=_("Recipe name"), required=True))
+        name=TextLine(title=_("Recipe name"), required=True),
+    )
     @operation_returns_entry(ICharmRecipe)
     @export_read_operation()
     @operation_for_version("devel")
@@ -626,7 +843,8 @@ class ICharmRecipeSet(Interface):
         """Check to see if a matching charm recipe exists."""
 
     @operation_parameters(
-        owner=Reference(IPerson, title=_("Owner"), required=True))
+        owner=Reference(IPerson, title=_("Owner"), required=True)
+    )
     @operation_returns_collection_of(ICharmRecipe)
     @export_read_operation()
     @operation_for_version("devel")
diff --git a/lib/lp/charms/interfaces/charmrecipebuild.py b/lib/lp/charms/interfaces/charmrecipebuild.py
index 2bbfe04..60574a8 100644
--- a/lib/lp/charms/interfaces/charmrecipebuild.py
+++ b/lib/lp/charms/interfaces/charmrecipebuild.py
@@ -9,52 +9,36 @@ __all__ = [
     "ICharmFile",
     "ICharmRecipeBuild",
     "ICharmRecipeBuildSet",
-    ]
+]
 
 import http.client
 
-from lazr.enum import (
-    EnumeratedType,
-    Item,
-    )
+from lazr.enum import EnumeratedType, Item
 from lazr.restful.declarations import (
     error_status,
     export_write_operation,
     exported,
     exported_as_webservice_entry,
     operation_for_version,
-    )
-from lazr.restful.fields import (
-    CollectionField,
-    Reference,
-    )
-from zope.interface import (
-    Attribute,
-    Interface,
-    )
-from zope.schema import (
-    Bool,
-    Choice,
-    Datetime,
-    Dict,
-    Int,
-    TextLine,
-    )
+)
+from lazr.restful.fields import CollectionField, Reference
+from zope.interface import Attribute, Interface
+from zope.schema import Bool, Choice, Datetime, Dict, Int, TextLine
 
 from lp import _
 from lp.buildmaster.interfaces.buildfarmjob import (
     IBuildFarmJobAdmin,
     IBuildFarmJobEdit,
     ISpecificBuildFarmJobSource,
-    )
+)
 from lp.buildmaster.interfaces.packagebuild import (
     IPackageBuild,
     IPackageBuildView,
-    )
+)
 from lp.charms.interfaces.charmrecipe import (
     ICharmRecipe,
     ICharmRecipeBuildRequest,
-    )
+)
 from lp.registry.interfaces.person import IPerson
 from lp.services.database.constants import DEFAULT
 from lp.services.librarian.interfaces import ILibraryFileAlias
@@ -73,37 +57,47 @@ class CharmRecipeBuildStoreUploadStatus(EnumeratedType):
     state of that process.
     """
 
-    UNSCHEDULED = Item("""
+    UNSCHEDULED = Item(
+        """
         Unscheduled
 
         No upload of this charm recipe build to Charmhub is scheduled.
-        """)
+        """
+    )
 
-    PENDING = Item("""
+    PENDING = Item(
+        """
         Pending
 
         This charm recipe build is queued for upload to Charmhub.
-        """)
+        """
+    )
 
-    FAILEDTOUPLOAD = Item("""
+    FAILEDTOUPLOAD = Item(
+        """
         Failed to upload
 
         The last attempt to upload this charm recipe build to Charmhub
         failed.
-        """)
+        """
+    )
 
-    FAILEDTORELEASE = Item("""
+    FAILEDTORELEASE = Item(
+        """
         Failed to release to channels
 
         The last attempt to release this charm recipe build to its intended
         set of channels failed.
-        """)
+        """
+    )
 
-    UPLOADED = Item("""
+    UPLOADED = Item(
+        """
         Uploaded
 
         This charm recipe build was successfully uploaded to Charmhub.
-        """)
+        """
+    )
 
 
 class ICharmRecipeBuildView(IPackageBuildView):
@@ -112,89 +106,140 @@ class ICharmRecipeBuildView(IPackageBuildView):
     build_request = Reference(
         ICharmRecipeBuildRequest,
         title=_("The build request that caused this build to be created."),
-        required=True, readonly=True)
+        required=True,
+        readonly=True,
+    )
 
-    requester = exported(Reference(
-        IPerson,
-        title=_("The person who requested this build."),
-        required=True, readonly=True))
+    requester = exported(
+        Reference(
+            IPerson,
+            title=_("The person who requested this build."),
+            required=True,
+            readonly=True,
+        )
+    )
 
-    recipe = exported(Reference(
-        ICharmRecipe,
-        title=_("The charm recipe to build."),
-        required=True, readonly=True))
+    recipe = exported(
+        Reference(
+            ICharmRecipe,
+            title=_("The charm recipe to build."),
+            required=True,
+            readonly=True,
+        )
+    )
 
-    distro_arch_series = exported(Reference(
-        IDistroArchSeries,
-        title=_("The series and architecture for which to build."),
-        required=True, readonly=True))
+    distro_arch_series = exported(
+        Reference(
+            IDistroArchSeries,
+            title=_("The series and architecture for which to build."),
+            required=True,
+            readonly=True,
+        )
+    )
 
     arch_tag = exported(
-        TextLine(title=_("Architecture tag"), required=True, readonly=True))
+        TextLine(title=_("Architecture tag"), required=True, readonly=True)
+    )
 
-    channels = exported(Dict(
-        title=_("Source snap channels to use for this build."),
-        description=_(
-            "A dictionary mapping snap names to channels to use for this "
-            "build.  Currently only 'charmcraft', 'core', 'core18', 'core20', "
-            "and 'core22' keys are supported."),
-        key_type=TextLine()))
+    channels = exported(
+        Dict(
+            title=_("Source snap channels to use for this build."),
+            description=_(
+                "A dictionary mapping snap names to channels to use for this "
+                "build.  Currently only 'charmcraft', 'core', 'core18', "
+                "'core20', and 'core22' keys are supported."
+            ),
+            key_type=TextLine(),
+        )
+    )
 
     virtualized = Bool(
-        title=_("If True, this build is virtualized."), readonly=True)
+        title=_("If True, this build is virtualized."), readonly=True
+    )
 
-    score = exported(Int(
-        title=_("Score of the related build farm job (if any)."),
-        required=False, readonly=True))
+    score = exported(
+        Int(
+            title=_("Score of the related build farm job (if any)."),
+            required=False,
+            readonly=True,
+        )
+    )
 
     eta = Datetime(
         title=_("The datetime when the build job is estimated to complete."),
-        readonly=True)
+        readonly=True,
+    )
 
     estimate = Bool(
-        title=_("If true, the date value is an estimate."), readonly=True)
+        title=_("If true, the date value is an estimate."), readonly=True
+    )
 
     date = Datetime(
         title=_(
-            "The date when the build completed or is estimated to complete."),
-        readonly=True)
+            "The date when the build completed or is estimated to complete."
+        ),
+        readonly=True,
+    )
 
-    revision_id = exported(TextLine(
-        title=_("Revision ID"), required=False, readonly=True,
-        description=_(
-            "The revision ID of the branch used for this build, if "
-            "available.")))
+    revision_id = exported(
+        TextLine(
+            title=_("Revision ID"),
+            required=False,
+            readonly=True,
+            description=_(
+                "The revision ID of the branch used for this build, if "
+                "available."
+            ),
+        )
+    )
 
     store_upload_jobs = CollectionField(
         title=_("Store upload jobs for this build."),
         # Really ICharmhubUploadJob.
         value_type=Reference(schema=Interface),
-        readonly=True)
+        readonly=True,
+    )
 
     # Really ICharmhubUploadJob.
     last_store_upload_job = Reference(
-        title=_("Last store upload job for this build."), schema=Interface)
-
-    store_upload_status = exported(Choice(
-        title=_("Store upload status"),
-        vocabulary=CharmRecipeBuildStoreUploadStatus,
-        required=True, readonly=False))
-
-    store_upload_revision = exported(Int(
-        title=_("Store revision"),
-        description=_(
-            "The revision assigned to this charm recipe build by Charmhub."),
-        required=False, readonly=True))
-
-    store_upload_error_message = exported(TextLine(
-        title=_("Store upload error message"),
-        description=_(
-            "The error message, if any, from the last attempt to upload "
-            "this charm recipe build to Charmhub."),
-        required=False, readonly=True))
+        title=_("Last store upload job for this build."), schema=Interface
+    )
+
+    store_upload_status = exported(
+        Choice(
+            title=_("Store upload status"),
+            vocabulary=CharmRecipeBuildStoreUploadStatus,
+            required=True,
+            readonly=False,
+        )
+    )
+
+    store_upload_revision = exported(
+        Int(
+            title=_("Store revision"),
+            description=_(
+                "The revision assigned to this charm recipe build by Charmhub."
+            ),
+            required=False,
+            readonly=True,
+        )
+    )
+
+    store_upload_error_message = exported(
+        TextLine(
+            title=_("Store upload error message"),
+            description=_(
+                "The error message, if any, from the last attempt to upload "
+                "this charm recipe build to Charmhub."
+            ),
+            required=False,
+            readonly=True,
+        )
+    )
 
     store_upload_metadata = Attribute(
-        _("A dict of data about store upload progress."))
+        _("A dict of data about store upload progress.")
+    )
 
     def getFiles():
         """Retrieve the build's `ICharmFile` records.
@@ -249,16 +294,25 @@ class ICharmRecipeBuildAdmin(IBuildFarmJobAdmin):
 # "devel".
 @exported_as_webservice_entry(as_of="beta")
 class ICharmRecipeBuild(
-        ICharmRecipeBuildView, ICharmRecipeBuildEdit, ICharmRecipeBuildAdmin,
-        IPackageBuild):
+    ICharmRecipeBuildView,
+    ICharmRecipeBuildEdit,
+    ICharmRecipeBuildAdmin,
+    IPackageBuild,
+):
     """A build record for a charm recipe."""
 
 
 class ICharmRecipeBuildSet(ISpecificBuildFarmJobSource):
     """Utility to create and access `ICharmRecipeBuild`s."""
 
-    def new(build_request, recipe, distro_arch_series, channels=None,
-            store_upload_metadata=None, date_created=DEFAULT):
+    def new(
+        build_request,
+        recipe,
+        distro_arch_series,
+        channels=None,
+        store_upload_metadata=None,
+        date_created=DEFAULT,
+    ):
         """Create an `ICharmRecipeBuild`."""
 
     def preloadBuildsData(builds):
@@ -271,8 +325,13 @@ class ICharmFile(Interface):
     build = Reference(
         ICharmRecipeBuild,
         title=_("The charm recipe build producing this file."),
-        required=True, readonly=True)
+        required=True,
+        readonly=True,
+    )
 
     library_file = Reference(
-        ILibraryFileAlias, title=_("the library file alias for this file."),
-        required=True, readonly=True)
+        ILibraryFileAlias,
+        title=_("the library file alias for this file."),
+        required=True,
+        readonly=True,
+    )
diff --git a/lib/lp/charms/interfaces/charmrecipebuildjob.py b/lib/lp/charms/interfaces/charmrecipebuildjob.py
index 40e0ece..eeef0c6 100644
--- a/lib/lp/charms/interfaces/charmrecipebuildjob.py
+++ b/lib/lp/charms/interfaces/charmrecipebuildjob.py
@@ -4,40 +4,36 @@
 """Charm recipe build job interfaces."""
 
 __all__ = [
-    'ICharmRecipeBuildJob',
-    'ICharmhubUploadJob',
-    'ICharmhubUploadJobSource',
-    ]
+    "ICharmRecipeBuildJob",
+    "ICharmhubUploadJob",
+    "ICharmhubUploadJobSource",
+]
 
 from lazr.restful.fields import Reference
-from zope.interface import (
-    Attribute,
-    Interface,
-    )
-from zope.schema import (
-    Int,
-    TextLine,
-    )
+from zope.interface import Attribute, Interface
+from zope.schema import Int, TextLine
 
 from lp import _
 from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
-from lp.services.job.interfaces.job import (
-    IJob,
-    IJobSource,
-    IRunnableJob,
-    )
+from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
 
 
 class ICharmRecipeBuildJob(Interface):
     """A job related to a charm recipe build."""
 
     job = Reference(
-        title=_("The common Job attributes."), schema=IJob,
-        required=True, readonly=True)
+        title=_("The common Job attributes."),
+        schema=IJob,
+        required=True,
+        readonly=True,
+    )
 
     build = Reference(
         title=_("The charm recipe build to use for this job."),
-        schema=ICharmRecipeBuild, required=True, readonly=True)
+        schema=ICharmRecipeBuild,
+        required=True,
+        readonly=True,
+    )
 
     metadata = Attribute(_("A dict of data about the job."))
 
@@ -46,31 +42,40 @@ class ICharmhubUploadJob(IRunnableJob):
     """A Job that uploads a charm recipe build to Charmhub."""
 
     store_metadata = Attribute(
-        _("Combined metadata for this job and the matching build"))
+        _("Combined metadata for this job and the matching build")
+    )
 
     error_message = TextLine(
-        title=_("Error message"), required=False, readonly=True)
+        title=_("Error message"), required=False, readonly=True
+    )
 
     error_detail = TextLine(
-        title=_("Error message detail"), required=False, readonly=True)
+        title=_("Error message detail"), required=False, readonly=True
+    )
 
     upload_id = Int(
         title=_(
             "The ID returned by Charmhub when uploading this build's charm "
-            "file."),
-        required=False, readonly=True)
+            "file."
+        ),
+        required=False,
+        readonly=True,
+    )
 
     status_url = TextLine(
         title=_("The URL on Charmhub to get the status of this build"),
-        required=False, readonly=True)
+        required=False,
+        readonly=True,
+    )
 
     store_revision = Int(
         title=_("The revision assigned to this build by Charmhub"),
-        required=False, readonly=True)
+        required=False,
+        readonly=True,
+    )
 
 
 class ICharmhubUploadJobSource(IJobSource):
-
     def create(build):
         """Upload a charm recipe build to Charmhub.
 
diff --git a/lib/lp/charms/interfaces/charmrecipejob.py b/lib/lp/charms/interfaces/charmrecipejob.py
index d353bb6..d1fb89e 100644
--- a/lib/lp/charms/interfaces/charmrecipejob.py
+++ b/lib/lp/charms/interfaces/charmrecipejob.py
@@ -7,45 +7,38 @@ __all__ = [
     "ICharmRecipeJob",
     "ICharmRecipeRequestBuildsJob",
     "ICharmRecipeRequestBuildsJobSource",
-    ]
+]
 
 from lazr.restful.fields import Reference
-from zope.interface import (
-    Attribute,
-    Interface,
-    )
-from zope.schema import (
-    Datetime,
-    Dict,
-    List,
-    Set,
-    TextLine,
-    )
+from zope.interface import Attribute, Interface
+from zope.schema import Datetime, Dict, List, Set, TextLine
 
 from lp import _
 from lp.charms.interfaces.charmrecipe import (
     ICharmRecipe,
     ICharmRecipeBuildRequest,
-    )
+)
 from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
 from lp.registry.interfaces.person import IPerson
-from lp.services.job.interfaces.job import (
-    IJob,
-    IJobSource,
-    IRunnableJob,
-    )
+from lp.services.job.interfaces.job import IJob, IJobSource, IRunnableJob
 
 
 class ICharmRecipeJob(Interface):
     """A job related to a charm recipe."""
 
     job = Reference(
-        title=_("The common Job attributes."), schema=IJob,
-        required=True, readonly=True)
+        title=_("The common Job attributes."),
+        schema=IJob,
+        required=True,
+        readonly=True,
+    )
 
     recipe = Reference(
         title=_("The charm recipe to use for this job."),
-        schema=ICharmRecipe, required=True, readonly=True)
+        schema=ICharmRecipe,
+        required=True,
+        readonly=True,
+    )
 
     metadata = Attribute(_("A dict of data about the job."))
 
@@ -54,45 +47,63 @@ class ICharmRecipeRequestBuildsJob(IRunnableJob):
     """A Job that processes a request for builds of a charm recipe."""
 
     requester = Reference(
-        title=_("The person requesting the builds."), schema=IPerson,
-        required=True, readonly=True)
+        title=_("The person requesting the builds."),
+        schema=IPerson,
+        required=True,
+        readonly=True,
+    )
 
     channels = Dict(
         title=_("Source snap channels to use for these builds."),
         description=_(
             "A dictionary mapping snap names to channels to use for these "
             "builds.  Currently only 'charmcraft', 'core', 'core18', "
-            "'core20', and 'core22' keys are supported."),
-        key_type=TextLine(), required=False, readonly=True)
+            "'core20', and 'core22' keys are supported."
+        ),
+        key_type=TextLine(),
+        required=False,
+        readonly=True,
+    )
 
     architectures = Set(
         title=_("If set, limit builds to these architecture tags."),
-        value_type=TextLine(), required=False, readonly=True)
+        value_type=TextLine(),
+        required=False,
+        readonly=True,
+    )
 
     date_created = Datetime(
         title=_("Time when this job was created."),
-        required=True, readonly=True)
+        required=True,
+        readonly=True,
+    )
 
     date_finished = Datetime(
-        title=_("Time when this job finished."),
-        required=True, readonly=True)
+        title=_("Time when this job finished."), required=True, readonly=True
+    )
 
     error_message = TextLine(
         title=_("Error message resulting from running this job."),
-        required=False, readonly=True)
+        required=False,
+        readonly=True,
+    )
 
     build_request = Reference(
         title=_("The build request corresponding to this job."),
-        schema=ICharmRecipeBuildRequest, required=True, readonly=True)
+        schema=ICharmRecipeBuildRequest,
+        required=True,
+        readonly=True,
+    )
 
     builds = List(
         title=_("The builds created by this request."),
         value_type=Reference(schema=ICharmRecipeBuild),
-        required=True, readonly=True)
+        required=True,
+        readonly=True,
+    )
 
 
 class ICharmRecipeRequestBuildsJobSource(IJobSource):
-
     def create(recipe, requester, channels=None, architectures=None):
         """Request builds of a charm recipe.
 
diff --git a/lib/lp/charms/interfaces/webservice.py b/lib/lp/charms/interfaces/webservice.py
index 5294ec1..5be9358 100644
--- a/lib/lp/charms/interfaces/webservice.py
+++ b/lib/lp/charms/interfaces/webservice.py
@@ -16,33 +16,32 @@ __all__ = [
     "ICharmRecipeBuild",
     "ICharmRecipeBuildRequest",
     "ICharmRecipeSet",
-    ]
+]
 
-from lp.charms.interfaces.charmbase import (
-    ICharmBase,
-    ICharmBaseSet,
-    )
+from lp.charms.interfaces.charmbase import ICharmBase, ICharmBaseSet
 from lp.charms.interfaces.charmrecipe import (
     ICharmRecipe,
     ICharmRecipeBuildRequest,
     ICharmRecipeSet,
     ICharmRecipeView,
-    )
+)
 from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
 from lp.services.webservice.apihelpers import (
     patch_collection_property,
     patch_reference_property,
-    )
-
+)
 
 # ICharmRecipeBuildRequest
 patch_reference_property(ICharmRecipeBuildRequest, "recipe", ICharmRecipe)
 patch_collection_property(
-    ICharmRecipeBuildRequest, "builds", ICharmRecipeBuild)
+    ICharmRecipeBuildRequest, "builds", ICharmRecipeBuild
+)
 
 # ICharmRecipeView
 patch_collection_property(ICharmRecipeView, "builds", ICharmRecipeBuild)
 patch_collection_property(
-    ICharmRecipeView, "completed_builds", ICharmRecipeBuild)
+    ICharmRecipeView, "completed_builds", ICharmRecipeBuild
+)
 patch_collection_property(
-    ICharmRecipeView, "pending_builds", ICharmRecipeBuild)
+    ICharmRecipeView, "pending_builds", ICharmRecipeBuild
+)
diff --git a/lib/lp/charms/mail/charmrecipebuild.py b/lib/lp/charms/mail/charmrecipebuild.py
index bb811ad..172d9af 100644
--- a/lib/lp/charms/mail/charmrecipebuild.py
+++ b/lib/lp/charms/mail/charmrecipebuild.py
@@ -3,14 +3,11 @@
 
 __all__ = [
     "CharmRecipeBuildMailer",
-    ]
+]
 
 from lp.app.browser.tales import DurationFormatterAPI
 from lp.services.config import config
-from lp.services.mail.basemailer import (
-    BaseMailer,
-    RecipientReason,
-    )
+from lp.services.mail.basemailer import BaseMailer, RecipientReason
 from lp.services.webapp import canonical_url
 
 
@@ -28,9 +25,12 @@ class CharmRecipeBuildMailer(BaseMailer):
         recipients = {requester: RecipientReason.forBuildRequester(requester)}
         return cls(
             "[Charm recipe build #%(build_id)d] %(build_title)s",
-            "charmrecipebuild-notification.txt", recipients,
-            config.canonical.noreply_from_address, "charm-recipe-build-status",
-            build)
+            "charmrecipebuild-notification.txt",
+            recipients,
+            config.canonical.noreply_from_address,
+            "charm-recipe-build-status",
+            build,
+        )
 
     @classmethod
     def forUnauthorizedUpload(cls, build):
@@ -42,9 +42,12 @@ class CharmRecipeBuildMailer(BaseMailer):
         recipients = {requester: RecipientReason.forBuildRequester(requester)}
         return cls(
             "Charmhub authorization failed for %(recipe_name)s",
-            "charmrecipebuild-unauthorized.txt", recipients,
+            "charmrecipebuild-unauthorized.txt",
+            recipients,
             config.canonical.noreply_from_address,
-            "charm-recipe-build-upload-unauthorized", build)
+            "charm-recipe-build-upload-unauthorized",
+            build,
+        )
 
     @classmethod
     def forUploadFailure(cls, build):
@@ -56,9 +59,12 @@ class CharmRecipeBuildMailer(BaseMailer):
         recipients = {requester: RecipientReason.forBuildRequester(requester)}
         return cls(
             "Charmhub upload failed for %(recipe_name)s",
-            "charmrecipebuild-uploadfailed.txt", recipients,
+            "charmrecipebuild-uploadfailed.txt",
+            recipients,
             config.canonical.noreply_from_address,
-            "charm-recipe-build-upload-failed", build)
+            "charm-recipe-build-upload-failed",
+            build,
+        )
 
     @classmethod
     def forUploadReviewFailure(cls, build):
@@ -70,9 +76,12 @@ class CharmRecipeBuildMailer(BaseMailer):
         recipients = {requester: RecipientReason.forBuildRequester(requester)}
         return cls(
             "Charmhub upload review failed for %(recipe_name)s",
-            "charmrecipebuild-reviewfailed.txt", recipients,
+            "charmrecipebuild-reviewfailed.txt",
+            recipients,
             config.canonical.noreply_from_address,
-            "charm-recipe-build-upload-review-failed", build)
+            "charm-recipe-build-upload-review-failed",
+            build,
+        )
 
     @classmethod
     def forReleaseFailure(cls, build):
@@ -84,15 +93,29 @@ class CharmRecipeBuildMailer(BaseMailer):
         recipients = {requester: RecipientReason.forBuildRequester(requester)}
         return cls(
             "Charmhub release failed for %(recipe_name)s",
-            "charmrecipebuild-releasefailed.txt", recipients,
+            "charmrecipebuild-releasefailed.txt",
+            recipients,
             config.canonical.noreply_from_address,
-            "charm-recipe-build-release-failed", build)
-
-    def __init__(self, subject, template_name, recipients, from_address,
-                 notification_type, build):
+            "charm-recipe-build-release-failed",
+            build,
+        )
+
+    def __init__(
+        self,
+        subject,
+        template_name,
+        recipients,
+        from_address,
+        notification_type,
+        build,
+    ):
         super().__init__(
-            subject, template_name, recipients, from_address,
-            notification_type=notification_type)
+            subject,
+            template_name,
+            recipients,
+            from_address,
+            notification_type=notification_type,
+        )
         self.build = build
 
     def _getHeaders(self, email, recipient):
@@ -110,23 +133,26 @@ class CharmRecipeBuildMailer(BaseMailer):
         else:
             error_message = upload_job.error_message or ""
         params = super()._getTemplateParams(email, recipient)
-        params.update({
-            "architecturetag": build.distro_arch_series.architecturetag,
-            "build_duration": "",
-            "build_id": build.id,
-            "build_state": build.status.title,
-            "build_title": build.title,
-            "build_url": canonical_url(build),
-            "builder_url": "",
-            "store_error_message": error_message,
-            "distroseries": build.distro_series,
-            "log_url": "",
-            "project_name": build.recipe.project.name,
-            "recipe_authorize_url": canonical_url(
-                build.recipe, view_name="+authorize"),
-            "recipe_name": build.recipe.name,
-            "upload_log_url": "",
-            })
+        params.update(
+            {
+                "architecturetag": build.distro_arch_series.architecturetag,
+                "build_duration": "",
+                "build_id": build.id,
+                "build_state": build.status.title,
+                "build_title": build.title,
+                "build_url": canonical_url(build),
+                "builder_url": "",
+                "store_error_message": error_message,
+                "distroseries": build.distro_series,
+                "log_url": "",
+                "project_name": build.recipe.project.name,
+                "recipe_authorize_url": canonical_url(
+                    build.recipe, view_name="+authorize"
+                ),
+                "recipe_name": build.recipe.name,
+                "upload_log_url": "",
+            }
+        )
         if build.duration is not None:
             duration_formatter = DurationFormatterAPI(build.duration)
             params["build_duration"] = duration_formatter.approximateduration()
@@ -140,5 +166,4 @@ class CharmRecipeBuildMailer(BaseMailer):
 
     def _getFooter(self, email, recipient, params):
         """See `BaseMailer`."""
-        return ("%(build_url)s\n"
-                "%(reason)s\n" % params)
+        return "%(build_url)s\n%(reason)s\n" % params
diff --git a/lib/lp/charms/model/charmbase.py b/lib/lp/charms/model/charmbase.py
index 6546b26..959b26c 100644
--- a/lib/lp/charms/model/charmbase.py
+++ b/lib/lp/charms/model/charmbase.py
@@ -5,17 +5,11 @@
 
 __all__ = [
     "CharmBase",
-    ]
+]
 
 import pytz
 from storm.databases.postgres import JSON
-from storm.locals import (
-    DateTime,
-    Int,
-    Reference,
-    Store,
-    Storm,
-    )
+from storm.locals import DateTime, Int, Reference, Store, Storm
 from zope.interface import implementer
 
 from lp.buildmaster.model.processor import Processor
@@ -24,12 +18,9 @@ from lp.charms.interfaces.charmbase import (
     ICharmBase,
     ICharmBaseSet,
     NoSuchCharmBase,
-    )
+)
 from lp.services.database.constants import DEFAULT
-from lp.services.database.interfaces import (
-    IMasterStore,
-    IStore,
-    )
+from lp.services.database.interfaces import IMasterStore, IStore
 
 
 @implementer(ICharmBase)
@@ -41,7 +32,8 @@ class CharmBase(Storm):
     id = Int(primary=True)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False)
+        name="date_created", tzinfo=pytz.UTC, allow_none=False
+    )
 
     registrant_id = Int(name="registrant", allow_none=False)
     registrant = Reference(registrant_id, "Person.id")
@@ -51,8 +43,13 @@ class CharmBase(Storm):
 
     build_snap_channels = JSON(name="build_snap_channels", allow_none=False)
 
-    def __init__(self, registrant, distro_series, build_snap_channels,
-                 date_created=DEFAULT):
+    def __init__(
+        self,
+        registrant,
+        distro_series,
+        build_snap_channels,
+        date_created=DEFAULT,
+    ):
         super().__init__()
         self.registrant = registrant
         self.distro_series = distro_series
@@ -60,17 +57,23 @@ class CharmBase(Storm):
         self.date_created = date_created
 
     def _getProcessors(self):
-        return list(Store.of(self).find(
-            Processor,
-            Processor.id == CharmBaseArch.processor_id,
-            CharmBaseArch.charm_base == self))
+        return list(
+            Store.of(self).find(
+                Processor,
+                Processor.id == CharmBaseArch.processor_id,
+                CharmBaseArch.charm_base == self,
+            )
+        )
 
     def setProcessors(self, processors):
         """See `ICharmBase`."""
-        enablements = dict(Store.of(self).find(
-            (Processor, CharmBaseArch),
-            Processor.id == CharmBaseArch.processor_id,
-            CharmBaseArch.charm_base == self))
+        enablements = dict(
+            Store.of(self).find(
+                (Processor, CharmBaseArch),
+                Processor.id == CharmBaseArch.processor_id,
+                CharmBaseArch.charm_base == self,
+            )
+        )
         for proc in enablements:
             if proc not in processors:
                 Store.of(self).remove(enablements[proc])
@@ -105,8 +108,14 @@ class CharmBaseArch(Storm):
 class CharmBaseSet:
     """See `ICharmBaseSet`."""
 
-    def new(self, registrant, distro_series, build_snap_channels,
-            processors=None, date_created=DEFAULT):
+    def new(
+        self,
+        registrant,
+        distro_series,
+        build_snap_channels,
+        processors=None,
+        date_created=DEFAULT,
+    ):
         """See `ICharmBaseSet`."""
         try:
             self.getByDistroSeries(distro_series)
@@ -116,12 +125,16 @@ class CharmBaseSet:
             raise DuplicateCharmBase(distro_series)
         store = IMasterStore(CharmBase)
         charm_base = CharmBase(
-            registrant, distro_series, build_snap_channels,
-            date_created=date_created)
+            registrant,
+            distro_series,
+            build_snap_channels,
+            date_created=date_created,
+        )
         store.add(charm_base)
         if processors is None:
             processors = [
-                das.processor for das in distro_series.enabled_architectures]
+                das.processor for das in distro_series.enabled_architectures
+            ]
         charm_base.setProcessors(processors)
         return charm_base
 
@@ -135,13 +148,19 @@ class CharmBaseSet:
 
     def getByDistroSeries(self, distro_series):
         """See `ICharmBaseSet`."""
-        charm_base = IStore(CharmBase).find(
-            CharmBase, distro_series=distro_series).one()
+        charm_base = (
+            IStore(CharmBase)
+            .find(CharmBase, distro_series=distro_series)
+            .one()
+        )
         if charm_base is None:
             raise NoSuchCharmBase(distro_series)
         return charm_base
 
     def getAll(self):
         """See `ICharmBaseSet`."""
-        return IStore(CharmBase).find(CharmBase).order_by(
-            CharmBase.distro_series_id)
+        return (
+            IStore(CharmBase)
+            .find(CharmBase)
+            .order_by(CharmBase.distro_series_id)
+        )
diff --git a/lib/lp/charms/model/charmhubclient.py b/lib/lp/charms/model/charmhubclient.py
index 1ac2f6c..cfc9567 100644
--- a/lib/lp/charms/model/charmhubclient.py
+++ b/lib/lp/charms/model/charmhubclient.py
@@ -5,15 +5,15 @@
 
 __all__ = [
     "CharmhubClient",
-    ]
+]
 
 from base64 import b64encode
 from urllib.parse import quote
 
+import requests
 from lazr.restful.utils import get_current_browser_request
 from pymacaroons import Macaroon
 from pymacaroons.serializers import JsonSerializer
-import requests
 from requests_toolbelt import MultipartEncoder
 from zope.component import getUtility
 from zope.interface import implementer
@@ -28,12 +28,9 @@ from lp.charms.interfaces.charmhubclient import (
     UnauthorizedUploadResponse,
     UploadFailedResponse,
     UploadNotReviewedYetResponse,
-    )
+)
 from lp.services.config import config
-from lp.services.crypto.interfaces import (
-    CryptoError,
-    IEncryptedContainer,
-    )
+from lp.services.crypto.interfaces import CryptoError, IEncryptedContainer
 from lp.services.librarian.utils import EncodableLibraryFileAlias
 from lp.services.timeline.requesttimeline import get_request_timeline
 from lp.services.timeout import urlfetch
@@ -46,13 +43,15 @@ def _get_macaroon(recipe):
     macaroon_raw = store_secrets.get("exchanged_encrypted")
     if macaroon_raw is None:
         raise UnauthorizedUploadResponse(
-            "{} is not authorized for upload to Charmhub".format(recipe))
+            "{} is not authorized for upload to Charmhub".format(recipe)
+        )
     container = getUtility(IEncryptedContainer, "charmhub-secrets")
     try:
         return container.decrypt(macaroon_raw).decode()
     except CryptoError as e:
         raise UnauthorizedUploadResponse(
-            "Failed to decrypt macaroon: {}".format(e))
+            "Failed to decrypt macaroon: {}".format(e)
+        )
 
 
 @implementer(ICharmhubClient)
@@ -80,7 +79,8 @@ class CharmhubClient:
                 if "error-list" in response_data:
                     error_message = "\n".join(
                         error["message"]
-                        for error in response_data["error-list"])
+                        for error in response_data["error-list"]
+                    )
         detail = requests_error.response.content.decode(errors="replace")
         can_retry = requests_error.response.status_code in (502, 503, 504)
         return error_class(error_message, detail=detail, can_retry=can_retry)
@@ -92,20 +92,24 @@ class CharmhubClient:
         request_url = urlappend(config.charms.charmhub_url, "v1/tokens")
         request = get_current_browser_request()
         timeline_action = get_request_timeline(request).start(
-            "request-charm-upload-macaroon", package_name, allow_nested=True)
+            "request-charm-upload-macaroon", package_name, allow_nested=True
+        )
         try:
             response = urlfetch(
-                request_url, method="POST",
+                request_url,
+                method="POST",
                 json={
                     "description": "{} for {}".format(
-                        package_name, config.vhost.mainsite.hostname),
+                        package_name, config.vhost.mainsite.hostname
+                    ),
                     "packages": [{"type": "charm", "name": package_name}],
                     "permissions": [
                         "package-manage-releases",
                         "package-manage-revisions",
                         "package-view-revisions",
-                        ],
-                    })
+                    ],
+                },
+            )
             response_data = response.json()
             if "macaroon" not in response_data:
                 raise BadRequestPackageUploadResponse(response.text)
@@ -116,27 +120,39 @@ class CharmhubClient:
             timeline_action.finish()
 
     @classmethod
-    def exchangeMacaroons(cls, root_macaroon_raw,
-                          unbound_discharge_macaroon_raw):
+    def exchangeMacaroons(
+        cls, root_macaroon_raw, unbound_discharge_macaroon_raw
+    ):
         """See `ICharmhubClient`."""
         assert config.charms.charmhub_url is not None
         root_macaroon = Macaroon.deserialize(
-            root_macaroon_raw, JsonSerializer())
+            root_macaroon_raw, JsonSerializer()
+        )
         unbound_discharge_macaroon = Macaroon.deserialize(
-            unbound_discharge_macaroon_raw, JsonSerializer())
+            unbound_discharge_macaroon_raw, JsonSerializer()
+        )
         discharge_macaroon_raw = root_macaroon.prepare_for_request(
-            unbound_discharge_macaroon).serialize(JsonSerializer())
+            unbound_discharge_macaroon
+        ).serialize(JsonSerializer())
         request_url = urlappend(
-            config.charms.charmhub_url, "v1/tokens/exchange")
+            config.charms.charmhub_url, "v1/tokens/exchange"
+        )
         request = get_current_browser_request()
         timeline_action = get_request_timeline(request).start(
-            "exchange-macaroons", "", allow_nested=True)
+            "exchange-macaroons", "", allow_nested=True
+        )
         try:
-            all_macaroons = b64encode("[{}, {}]".format(
-                root_macaroon_raw, discharge_macaroon_raw).encode()).decode()
+            all_macaroons = b64encode(
+                "[{}, {}]".format(
+                    root_macaroon_raw, discharge_macaroon_raw
+                ).encode()
+            ).decode()
             response = urlfetch(
-                request_url, method="POST",
-                headers={"Macaroons": all_macaroons}, json={})
+                request_url,
+                method="POST",
+                headers={"Macaroons": all_macaroons},
+                json={},
+            )
             response_data = response.json()
             if "macaroon" not in response_data:
                 raise BadExchangeMacaroonsResponse(response.text)
@@ -151,25 +167,34 @@ class CharmhubClient:
         """Upload a single file to Charmhub's storage."""
         assert config.charms.charmhub_storage_url is not None
         unscanned_upload_url = urlappend(
-            config.charms.charmhub_storage_url, "unscanned-upload/")
+            config.charms.charmhub_storage_url, "unscanned-upload/"
+        )
         lfa.open()
         try:
             lfa_wrapper = EncodableLibraryFileAlias(lfa)
             encoder = MultipartEncoder(
                 fields={
                     "binary": (
-                        lfa.filename, lfa_wrapper, "application/octet-stream"),
-                    })
+                        lfa.filename,
+                        lfa_wrapper,
+                        "application/octet-stream",
+                    ),
+                }
+            )
             request = get_current_browser_request()
             timeline_action = get_request_timeline(request).start(
-                "charm-storage-push", lfa.filename, allow_nested=True)
+                "charm-storage-push", lfa.filename, allow_nested=True
+            )
             try:
                 response = urlfetch(
-                    unscanned_upload_url, method="POST", data=encoder,
+                    unscanned_upload_url,
+                    method="POST",
+                    data=encoder,
                     headers={
                         "Content-Type": encoder.content_type,
                         "Accept": "application/json",
-                        })
+                    },
+                )
                 response_data = response.json()
                 if not response_data.get("successful", False):
                     raise UploadFailedResponse(response.text)
@@ -188,17 +213,21 @@ class CharmhubClient:
         assert recipe.can_upload_to_store
         push_url = urlappend(
             config.charms.charmhub_url,
-            "v1/charm/{}/revisions".format(quote(recipe.store_name)))
+            "v1/charm/{}/revisions".format(quote(recipe.store_name)),
+        )
         macaroon_raw = _get_macaroon(recipe)
         data = {"upload-id": upload_id}
         request = get_current_browser_request()
         timeline_action = get_request_timeline(request).start(
-            "charm-push", recipe.store_name, allow_nested=True)
+            "charm-push", recipe.store_name, allow_nested=True
+        )
         try:
             response = urlfetch(
-                push_url, method="POST",
+                push_url,
+                method="POST",
                 headers={"Authorization": "Macaroon {}".format(macaroon_raw)},
-                json=data)
+                json=data,
+            )
             response_data = response.json()
             return response_data["status-url"]
         except requests.HTTPError as e:
@@ -213,15 +242,18 @@ class CharmhubClient:
     def checkStatus(cls, build, status_url):
         """See `ICharmhubClient`."""
         status_url = urlappend(
-            config.charms.charmhub_url, status_url.lstrip("/"))
+            config.charms.charmhub_url, status_url.lstrip("/")
+        )
         macaroon_raw = _get_macaroon(build.recipe)
         request = get_current_browser_request()
         timeline_action = get_request_timeline(request).start(
-            "charm-check-status", status_url, allow_nested=True)
+            "charm-check-status", status_url, allow_nested=True
+        )
         try:
             response = urlfetch(
                 status_url,
-                headers={"Authorization": "Macaroon {}".format(macaroon_raw)})
+                headers={"Authorization": "Macaroon {}".format(macaroon_raw)},
+            )
             response_data = response.json()
             # We're asking for a single upload ID, so the response should
             # only have one revision.
@@ -231,11 +263,13 @@ class CharmhubClient:
             if revision["status"] == "approved":
                 if revision["revision"] is None:
                     raise ReviewFailedResponse(
-                        "Review passed but did not assign a revision.")
+                        "Review passed but did not assign a revision."
+                    )
                 return revision["revision"]
             elif revision["status"] == "rejected":
                 error_message = "\n".join(
-                    error["message"] for error in revision["errors"])
+                    error["message"] for error in revision["errors"]
+                )
                 raise ReviewFailedResponse(error_message)
             else:
                 raise UploadNotReviewedYetResponse()
@@ -254,19 +288,24 @@ class CharmhubClient:
         assert recipe.store_channels
         release_url = urlappend(
             config.charms.charmhub_url,
-            "v1/charm/{}/releases".format(quote(recipe.store_name)))
+            "v1/charm/{}/releases".format(quote(recipe.store_name)),
+        )
         macaroon_raw = _get_macaroon(recipe)
         data = [
             {"channel": channel, "revision": revision}
-            for channel in recipe.store_channels]
+            for channel in recipe.store_channels
+        ]
         request = get_current_browser_request()
         timeline_action = get_request_timeline(request).start(
-            "charm-release", recipe.store_name, allow_nested=True)
+            "charm-release", recipe.store_name, allow_nested=True
+        )
         try:
             urlfetch(
-                release_url, method="POST",
+                release_url,
+                method="POST",
                 headers={"Authorization": "Macaroon {}".format(macaroon_raw)},
-                json=data)
+                json=data,
+            )
         except requests.HTTPError as e:
             raise cls._makeCharmhubError(ReleaseFailedResponse, e)
         finally:
diff --git a/lib/lp/charms/model/charmrecipe.py b/lib/lp/charms/model/charmrecipe.py
index f1bc919..1b4f0bb 100644
--- a/lib/lp/charms/model/charmrecipe.py
+++ b/lib/lp/charms/model/charmrecipe.py
@@ -6,28 +6,19 @@
 __all__ = [
     "CharmRecipe",
     "get_charm_recipe_privacy_filter",
-    ]
+]
 
 import base64
-from datetime import (
-    datetime,
-    timedelta,
-    )
-from operator import (
-    attrgetter,
-    itemgetter,
-    )
+from datetime import datetime, timedelta
+from operator import attrgetter, itemgetter
 
+import pytz
+import yaml
 from lazr.lifecycle.event import ObjectCreatedEvent
 from pymacaroons import Macaroon
 from pymacaroons.serializers import JsonSerializer
-import pytz
 from storm.databases.postgres import JSON
-from storm.expr import (
-    Cast,
-    Coalesce,
-    Except,
-    )
+from storm.expr import Cast, Coalesce, Except
 from storm.locals import (
     And,
     Bool,
@@ -41,8 +32,7 @@ from storm.locals import (
     Select,
     Store,
     Unicode,
-    )
-import yaml
+)
 from zope.component import getUtility
 from zope.event import notify
 from zope.interface import implementer
@@ -50,9 +40,9 @@ from zope.security.proxy import removeSecurityProxy
 
 from lp.app.enums import (
     FREE_INFORMATION_TYPES,
-    InformationType,
     PUBLIC_INFORMATION_TYPES,
-    )
+    InformationType,
+)
 from lp.app.interfaces.launchpad import ILaunchpadCelebrities
 from lp.buildmaster.enums import BuildStatus
 from lp.buildmaster.interfaces.buildqueue import IBuildQueueSet
@@ -60,19 +50,16 @@ from lp.buildmaster.model.builder import Builder
 from lp.buildmaster.model.buildfarmjob import BuildFarmJob
 from lp.buildmaster.model.buildqueue import BuildQueue
 from lp.charms.adapters.buildarch import determine_instances_to_build
-from lp.charms.interfaces.charmbase import (
-    ICharmBaseSet,
-    NoSuchCharmBase,
-    )
+from lp.charms.interfaces.charmbase import ICharmBaseSet, NoSuchCharmBase
 from lp.charms.interfaces.charmhubclient import ICharmhubClient
 from lp.charms.interfaces.charmrecipe import (
+    CHARM_RECIPE_ALLOW_CREATE,
+    CHARM_RECIPE_BUILD_DISTRIBUTION,
+    CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
     BadCharmRecipeSearchContext,
     CannotAuthorizeCharmhubUploads,
     CannotFetchCharmcraftYaml,
     CannotParseCharmcraftYaml,
-    CHARM_RECIPE_ALLOW_CREATE,
-    CHARM_RECIPE_BUILD_DISTRIBUTION,
-    CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
     CharmRecipeBuildAlreadyPending,
     CharmRecipeBuildDisallowedArchitecture,
     CharmRecipeBuildRequestStatus,
@@ -87,25 +74,19 @@ from lp.charms.interfaces.charmrecipe import (
     MissingCharmcraftYaml,
     NoSourceForCharmRecipe,
     NoSuchCharmRecipe,
-    )
+)
 from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuildSet
 from lp.charms.interfaces.charmrecipejob import (
     ICharmRecipeRequestBuildsJobSource,
-    )
+)
 from lp.charms.model.charmbase import CharmBase
 from lp.charms.model.charmrecipebuild import CharmRecipeBuild
-from lp.charms.model.charmrecipejob import (
-    CharmRecipeJob,
-    CharmRecipeJobType,
-    )
-from lp.code.errors import (
-    GitRepositoryBlobNotFound,
-    GitRepositoryScanFault,
-    )
+from lp.charms.model.charmrecipejob import CharmRecipeJob, CharmRecipeJobType
+from lp.code.errors import GitRepositoryBlobNotFound, GitRepositoryScanFault
 from lp.code.interfaces.gitcollection import (
     IAllGitRepositories,
     IGitCollection,
-    )
+)
 from lp.code.interfaces.gitref import IGitRef
 from lp.code.interfaces.gitrepository import IGitRepository
 from lp.code.model.gitcollection import GenericGitCollection
@@ -117,7 +98,7 @@ from lp.registry.interfaces.person import (
     IPerson,
     IPersonSet,
     validate_public_person,
-    )
+)
 from lp.registry.interfaces.product import IProduct
 from lp.registry.model.distribution import Distribution
 from lp.registry.model.distroseries import DistroSeries
@@ -127,38 +108,26 @@ from lp.services.config import config
 from lp.services.crypto.interfaces import IEncryptedContainer
 from lp.services.crypto.model import NaClEncryptedContainerBase
 from lp.services.database.bulk import load_related
-from lp.services.database.constants import (
-    DEFAULT,
-    UTC_NOW,
-    )
+from lp.services.database.constants import DEFAULT, UTC_NOW
 from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
-    IMasterStore,
-    IStore,
-    )
+from lp.services.database.interfaces import IMasterStore, IStore
 from lp.services.database.stormbase import StormBase
 from lp.services.database.stormexpr import (
     Greatest,
     IsTrue,
     JSONExtract,
     NullsLast,
-    )
+)
 from lp.services.features import getFeatureFlag
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.model.job import Job
 from lp.services.librarian.model import LibraryFileAlias
-from lp.services.propertycache import (
-    cachedproperty,
-    get_property_cache,
-    )
+from lp.services.propertycache import cachedproperty, get_property_cache
 from lp.services.webapp.candid import extract_candid_caveat
 from lp.services.webhooks.interfaces import IWebhookSet
 from lp.services.webhooks.model import WebhookTargetMixin
-from lp.soyuz.model.distroarchseries import (
-    DistroArchSeries,
-    PocketChroot,
-    )
+from lp.soyuz.model.distroarchseries import DistroArchSeries, PocketChroot
 
 
 def charm_recipe_modified(recipe, event):
@@ -213,7 +182,7 @@ class CharmRecipeBuildRequest:
             JobStatus.COMPLETED: CharmRecipeBuildRequestStatus.COMPLETED,
             JobStatus.FAILED: CharmRecipeBuildRequestStatus.FAILED,
             JobStatus.SUSPENDED: CharmRecipeBuildRequestStatus.PENDING,
-            }
+        }
         return status_map[self._job.job.status]
 
     @property
@@ -251,9 +220,11 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
     id = Int(primary=True)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False)
+        name="date_created", tzinfo=pytz.UTC, allow_none=False
+    )
     date_last_modified = DateTime(
-        name="date_last_modified", tzinfo=pytz.UTC, allow_none=False)
+        name="date_last_modified", tzinfo=pytz.UTC, allow_none=False
+    )
 
     registrant_id = Int(name="registrant", allow_none=False)
     registrant = Reference(registrant_id, "Person.id")
@@ -264,7 +235,8 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
                 validate_public_person(self, attr, value)
             except PrivatePersonLinkageError:
                 raise CharmRecipePrivacyMismatch(
-                    "A public charm recipe cannot have a private owner.")
+                    "A public charm recipe cannot have a private owner."
+                )
         return value
 
     owner_id = Int(name="owner", allow_none=False, validator=_validate_owner)
@@ -281,12 +253,15 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         if not self.private and value is not None:
             if IStore(GitRepository).get(GitRepository, value).private:
                 raise CharmRecipePrivacyMismatch(
-                    "A public charm recipe cannot have a private repository.")
+                    "A public charm recipe cannot have a private repository."
+                )
         return value
 
     git_repository_id = Int(
-        name="git_repository", allow_none=True,
-        validator=_validate_git_repository)
+        name="git_repository",
+        allow_none=True,
+        validator=_validate_git_repository,
+    )
     git_repository = Reference(git_repository_id, "GitRepository.id")
 
     git_path = Unicode(name="git_path", allow_none=True)
@@ -297,14 +272,18 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
 
     def _valid_information_type(self, attr, value):
         if not getUtility(ICharmRecipeSet).isValidInformationType(
-                value, self.owner, self.git_ref):
+            value, self.owner, self.git_ref
+        ):
             raise CharmRecipePrivacyMismatch
         return value
 
     information_type = DBEnum(
-        enum=InformationType, default=InformationType.PUBLIC,
-        name="information_type", validator=_valid_information_type,
-        allow_none=False)
+        enum=InformationType,
+        default=InformationType.PUBLIC,
+        name="information_type",
+        validator=_valid_information_type,
+        allow_none=False,
+    )
 
     auto_build = Bool(name="auto_build", allow_none=False)
 
@@ -320,12 +299,25 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
 
     _store_channels = JSON("store_channels", allow_none=True)
 
-    def __init__(self, registrant, owner, project, name, description=None,
-                 git_ref=None, build_path=None, require_virtualized=True,
-                 information_type=InformationType.PUBLIC, auto_build=False,
-                 auto_build_channels=None, store_upload=False,
-                 store_name=None, store_secrets=None, store_channels=None,
-                 date_created=DEFAULT):
+    def __init__(
+        self,
+        registrant,
+        owner,
+        project,
+        name,
+        description=None,
+        git_ref=None,
+        build_path=None,
+        require_virtualized=True,
+        information_type=InformationType.PUBLIC,
+        auto_build=False,
+        auto_build_channels=None,
+        store_upload=False,
+        store_name=None,
+        store_secrets=None,
+        store_channels=None,
+        date_created=DEFAULT,
+    ):
         """Construct a `CharmRecipe`."""
         if not getFeatureFlag(CHARM_RECIPE_ALLOW_CREATE):
             raise CharmRecipeFeatureDisabled()
@@ -353,7 +345,10 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
 
     def __repr__(self):
         return "<CharmRecipe ~%s/%s/+charm/%s>" % (
-            self.owner.name, self.project.name, self.name)
+            self.owner.name,
+            self.project.name,
+            self.name,
+        )
 
     @property
     def private(self):
@@ -409,8 +404,9 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         distro = getUtility(IDistributionSet).getByName(distro_name)
         if not distro:
             raise ValueError(
-                "'%s' is not a valid value for feature rule '%s'" % (
-                    distro_name, CHARM_RECIPE_BUILD_DISTRIBUTION))
+                "'%s' is not a valid value for feature rule '%s'"
+                % (distro_name, CHARM_RECIPE_BUILD_DISTRIBUTION)
+            )
         return distro
 
     @cachedproperty
@@ -419,7 +415,8 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         # Use the series set by this feature rule, or the current series of
         # the default distribution if the feature rule is not set.
         series_name = getFeatureFlag(
-            "charm.default_build_series.%s" % self._default_distribution.name)
+            "charm.default_build_series.%s" % self._default_distribution.name
+        )
         if series_name:
             return self._default_distribution.getSeries(series_name)
         else:
@@ -437,10 +434,15 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
             return True
         if user is None:
             return False
-        return not IStore(CharmRecipe).find(
-            CharmRecipe,
-            CharmRecipe.id == self.id,
-            get_charm_recipe_privacy_filter(user)).is_empty()
+        return (
+            not IStore(CharmRecipe)
+            .find(
+                CharmRecipe,
+                CharmRecipe.id == self.id,
+                get_charm_recipe_privacy_filter(user),
+            )
+            .is_empty()
+        )
 
     def _isBuildableArchitectureAllowed(self, das, charm_base=None):
         """Check whether we may build for a buildable `DistroArchSeries`.
@@ -453,55 +455,74 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
             das.enabled
             and (
                 das.processor.supports_virtualized
-                or not self.require_virtualized)
-            and (charm_base is None or das.processor in charm_base.processors))
+                or not self.require_virtualized
+            )
+            and (charm_base is None or das.processor in charm_base.processors)
+        )
 
     def _isArchitectureAllowed(self, das, charm_base=None):
         """Check whether we may build for a `DistroArchSeries`."""
         return (
             das.getChroot() is not None
             and self._isBuildableArchitectureAllowed(
-                das, charm_base=charm_base))
+                das, charm_base=charm_base
+            )
+        )
 
     def getAllowedArchitectures(self):
         """See `ICharmRecipe`."""
         store = Store.of(self)
         origin = [
             DistroArchSeries,
-            Join(DistroSeries,
-                 DistroArchSeries.distroseries == DistroSeries.id),
+            Join(
+                DistroSeries, DistroArchSeries.distroseries == DistroSeries.id
+            ),
             Join(Distribution, DistroSeries.distribution == Distribution.id),
-            Join(PocketChroot,
-                 PocketChroot.distroarchseries == DistroArchSeries.id),
-            Join(LibraryFileAlias,
-                 PocketChroot.chroot == LibraryFileAlias.id),
-            ]
+            Join(
+                PocketChroot,
+                PocketChroot.distroarchseries == DistroArchSeries.id,
+            ),
+            Join(LibraryFileAlias, PocketChroot.chroot == LibraryFileAlias.id),
+        ]
         # Preload DistroSeries and Distribution, since we'll need those in
         # determine_architectures_to_build.
-        results = store.using(*origin).find(
-            (DistroArchSeries, DistroSeries, Distribution),
-            DistroSeries.status.is_in(ACTIVE_STATUSES)).config(distinct=True)
+        results = (
+            store.using(*origin)
+            .find(
+                (DistroArchSeries, DistroSeries, Distribution),
+                DistroSeries.status.is_in(ACTIVE_STATUSES),
+            )
+            .config(distinct=True)
+        )
         all_buildable_dases = DecoratedResultSet(results, itemgetter(0))
         charm_bases = {
             charm_base.distro_series_id: charm_base
             for charm_base in store.find(
                 CharmBase,
                 CharmBase.distro_series_id.is_in(
-                    {das.distroseriesID for das in all_buildable_dases}))}
+                    {das.distroseriesID for das in all_buildable_dases}
+                ),
+            )
+        }
         return [
-            das for das in all_buildable_dases
+            das
+            for das in all_buildable_dases
             if self._isBuildableArchitectureAllowed(
-                das, charm_base=charm_bases.get(das.distroseriesID))]
+                das, charm_base=charm_bases.get(das.distroseriesID)
+            )
+        ]
 
     def _checkRequestBuild(self, requester):
         """May `requester` request builds of this charm recipe?"""
         if not requester.inTeam(self.owner):
             raise CharmRecipeNotOwner(
-                "%s cannot create charm recipe builds owned by %s." %
-                (requester.display_name, self.owner.display_name))
+                "%s cannot create charm recipe builds owned by %s."
+                % (requester.display_name, self.owner.display_name)
+            )
 
-    def requestBuild(self, build_request, distro_arch_series, charm_base=None,
-                     channels=None):
+    def requestBuild(
+        self, build_request, distro_arch_series, charm_base=None, channels=None
+    ):
         """Request a single build of this charm recipe.
 
         This method is for internal use; external callers should use
@@ -516,13 +537,15 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         """
         self._checkRequestBuild(build_request.requester)
         if not self._isArchitectureAllowed(
-                distro_arch_series, charm_base=charm_base):
+            distro_arch_series, charm_base=charm_base
+        ):
             raise CharmRecipeBuildDisallowedArchitecture(distro_arch_series)
 
         if not channels:
             channels_clause = Or(
                 CharmRecipeBuild.channels == None,
-                CharmRecipeBuild.channels == {})
+                CharmRecipeBuild.channels == {},
+            )
         else:
             channels_clause = CharmRecipeBuild.channels == channels
         pending = IStore(self).find(
@@ -530,12 +553,14 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
             CharmRecipeBuild.recipe == self,
             CharmRecipeBuild.processor == distro_arch_series.processor,
             channels_clause,
-            CharmRecipeBuild.status == BuildStatus.NEEDSBUILD)
+            CharmRecipeBuild.status == BuildStatus.NEEDSBUILD,
+        )
         if pending.any() is not None:
             raise CharmRecipeBuildAlreadyPending
 
         build = getUtility(ICharmRecipeBuildSet).new(
-            build_request, self, distro_arch_series, channels=channels)
+            build_request, self, distro_arch_series, channels=channels
+        )
         build.queueBuild()
         notify(ObjectCreatedEvent(build, user=build_request.requester))
         return build
@@ -544,17 +569,24 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         """See `ICharmRecipe`."""
         self._checkRequestBuild(requester)
         job = getUtility(ICharmRecipeRequestBuildsJobSource).create(
-            self, requester, channels=channels, architectures=architectures)
+            self, requester, channels=channels, architectures=architectures
+        )
         return self.getBuildRequest(job.job_id)
 
-    def requestBuildsFromJob(self, build_request, channels=None,
-                             architectures=None, allow_failures=False,
-                             logger=None):
+    def requestBuildsFromJob(
+        self,
+        build_request,
+        channels=None,
+        architectures=None,
+        allow_failures=False,
+        logger=None,
+    ):
         """See `ICharmRecipe`."""
         try:
             try:
                 charmcraft_data = removeSecurityProxy(
-                    getUtility(ICharmRecipeSet).getCharmcraftYaml(self))
+                    getUtility(ICharmRecipeSet).getCharmcraftYaml(self)
+                )
             except MissingCharmcraftYaml:
                 # charmcraft doesn't currently require charmcraft.yaml, and
                 # we have reasonable defaults without it.
@@ -564,28 +596,41 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
             # determinism.  This is chosen to be a similar order as in
             # BinaryPackageBuildSet.createForSource, to minimize confusion.
             supported_arches = [
-                das for das in sorted(
+                das
+                for das in sorted(
                     self.getAllowedArchitectures(),
                     key=attrgetter(
-                        "distroseries.distribution.id", "distroseries.id",
-                        "processor.id"))
-                if (architectures is None or
-                    das.architecturetag in architectures)]
+                        "distroseries.distribution.id",
+                        "distroseries.id",
+                        "processor.id",
+                    ),
+                )
+                if (
+                    architectures is None
+                    or das.architecturetag in architectures
+                )
+            ]
             instances_to_build = determine_instances_to_build(
-                charmcraft_data, supported_arches, self._default_distro_series)
+                charmcraft_data, supported_arches, self._default_distro_series
+            )
         except Exception as e:
             if not allow_failures:
                 raise
             elif logger is not None:
                 logger.exception(
                     " - %s/%s/%s: %s",
-                    self.owner.name, self.project.name, self.name, e)
+                    self.owner.name,
+                    self.project.name,
+                    self.name,
+                    e,
+                )
 
         builds = []
         for das in instances_to_build:
             try:
                 charm_base = getUtility(ICharmBaseSet).getByDistroSeries(
-                    das.distroseries)
+                    das.distroseries
+                )
             except NoSuchCharmBase:
                 charm_base = None
             if charm_base is not None:
@@ -599,13 +644,18 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
                 arch_channels = channels
             try:
                 build = self.requestBuild(
-                    build_request, das, channels=arch_channels)
+                    build_request, das, channels=arch_channels
+                )
                 if logger is not None:
                     logger.debug(
                         " - %s/%s/%s %s/%s/%s: Build requested.",
-                        self.owner.name, self.project.name, self.name,
+                        self.owner.name,
+                        self.project.name,
+                        self.name,
                         das.distroseries.distribution.name,
-                        das.distroseries.name, das.architecturetag)
+                        das.distroseries.name,
+                        das.architecturetag,
+                    )
                 builds.append(build)
             except CharmRecipeBuildAlreadyPending:
                 pass
@@ -615,9 +665,14 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
                 elif logger is not None:
                     logger.exception(
                         " - %s/%s/%s %s/%s/%s: %s",
-                        self.owner.name, self.project.name, self.name,
+                        self.owner.name,
+                        self.project.name,
+                        self.name,
                         das.distroseries.distribution.name,
-                        das.distroseries.name, das.architecturetag, e)
+                        das.distroseries.name,
+                        das.architecturetag,
+                        e,
+                    )
         return builds
 
     def requestAutoBuilds(self, logger=None):
@@ -626,9 +681,13 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         if logger is not None:
             logger.debug(
                 "Scheduling builds of charm recipe %s/%s/%s",
-                self.owner.name, self.project.name, self.name)
+                self.owner.name,
+                self.project.name,
+                self.name,
+            )
         return self.requestBuilds(
-            requester=self.owner, channels=self.auto_build_channels)
+            requester=self.owner, channels=self.auto_build_channels
+        )
 
     def getBuildRequest(self, job_id):
         """See `ICharmRecipe`."""
@@ -640,9 +699,11 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         job_source = getUtility(ICharmRecipeRequestBuildsJobSource)
         # The returned jobs are ordered by descending ID.
         jobs = job_source.findByRecipe(
-            self, statuses=(JobStatus.WAITING, JobStatus.RUNNING))
+            self, statuses=(JobStatus.WAITING, JobStatus.RUNNING)
+        )
         return DecoratedResultSet(
-            jobs, result_decorator=CharmRecipeBuildRequest.fromJob)
+            jobs, result_decorator=CharmRecipeBuildRequest.fromJob
+        )
 
     @property
     def failed_build_requests(self):
@@ -651,13 +712,14 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         # The returned jobs are ordered by descending ID.
         jobs = job_source.findByRecipe(self, statuses=(JobStatus.FAILED,))
         return DecoratedResultSet(
-            jobs, result_decorator=CharmRecipeBuildRequest.fromJob)
+            jobs, result_decorator=CharmRecipeBuildRequest.fromJob
+        )
 
     def _getBuilds(self, filter_term, order_by):
         """The actual query to get the builds."""
         query_args = [
             CharmRecipeBuild.recipe == self,
-            ]
+        ]
         if filter_term is not None:
             query_args.append(filter_term)
         result = Store.of(self).find(CharmRecipeBuild, *query_args)
@@ -674,11 +736,17 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
     def builds(self):
         """See `ICharmRecipe`."""
         order_by = (
-            NullsLast(Desc(Greatest(
-                CharmRecipeBuild.date_started,
-                CharmRecipeBuild.date_finished))),
+            NullsLast(
+                Desc(
+                    Greatest(
+                        CharmRecipeBuild.date_started,
+                        CharmRecipeBuild.date_finished,
+                    )
+                )
+            ),
             Desc(CharmRecipeBuild.date_created),
-            Desc(CharmRecipeBuild.id))
+            Desc(CharmRecipeBuild.id),
+        )
         return self._getBuilds(None, order_by)
 
     @property
@@ -689,23 +757,29 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
             BuildStatus.BUILDING,
             BuildStatus.UPLOADING,
             BuildStatus.CANCELLING,
-            ]
+        ]
 
     @property
     def completed_builds(self):
         """See `ICharmRecipe`."""
         filter_term = Not(CharmRecipeBuild.status.is_in(self._pending_states))
         order_by = (
-            NullsLast(Desc(Greatest(
-                CharmRecipeBuild.date_started,
-                CharmRecipeBuild.date_finished))),
-            Desc(CharmRecipeBuild.id))
+            NullsLast(
+                Desc(
+                    Greatest(
+                        CharmRecipeBuild.date_started,
+                        CharmRecipeBuild.date_finished,
+                    )
+                )
+            ),
+            Desc(CharmRecipeBuild.id),
+        )
         return self._getBuilds(filter_term, order_by)
 
     @property
     def pending_builds(self):
         """See `ICharmRecipe`."""
-        filter_term = (CharmRecipeBuild.status.is_in(self._pending_states))
+        filter_term = CharmRecipeBuild.status.is_in(self._pending_states)
         # We want to order by date_created but this is the same as ordering
         # by id (since id increases monotonically) and is less expensive.
         order_by = Desc(CharmRecipeBuild.id)
@@ -716,13 +790,16 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         if self.store_name is None:
             raise CannotAuthorizeCharmhubUploads(
                 "Cannot authorize uploads of a charm recipe with no store "
-                "name.")
+                "name."
+            )
         charmhub_client = getUtility(ICharmhubClient)
         root_macaroon_raw = charmhub_client.requestPackageUploadPermission(
-            self.store_name)
+            self.store_name
+        )
         # Check that the macaroon has exactly one Candid caveat.
         extract_candid_caveat(
-            Macaroon.deserialize(root_macaroon_raw, JsonSerializer()))
+            Macaroon.deserialize(root_macaroon_raw, JsonSerializer())
+        )
         self.store_secrets = {"root": root_macaroon_raw}
         return root_macaroon_raw
 
@@ -731,29 +808,35 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         if self.store_secrets is None or "root" not in self.store_secrets:
             raise CannotAuthorizeCharmhubUploads(
                 "beginAuthorization must be called before "
-                "completeAuthorization.")
+                "completeAuthorization."
+            )
         try:
             Macaroon.deserialize(
-                unbound_discharge_macaroon_raw, JsonSerializer())
+                unbound_discharge_macaroon_raw, JsonSerializer()
+            )
         except Exception:
             raise CannotAuthorizeCharmhubUploads(
-                "Discharge macaroon is invalid.")
+                "Discharge macaroon is invalid."
+            )
         charmhub_client = getUtility(ICharmhubClient)
         exchanged_macaroon_raw = charmhub_client.exchangeMacaroons(
-            self.store_secrets["root"], unbound_discharge_macaroon_raw)
+            self.store_secrets["root"], unbound_discharge_macaroon_raw
+        )
         container = getUtility(IEncryptedContainer, "charmhub-secrets")
         assert container.can_encrypt
         self.store_secrets["exchanged_encrypted"] = removeSecurityProxy(
-            container.encrypt(exchanged_macaroon_raw.encode()))
+            container.encrypt(exchanged_macaroon_raw.encode())
+        )
         self.store_secrets.pop("root", None)
 
     @property
     def can_upload_to_store(self):
         return (
-            config.charms.charmhub_url is not None and
-            self.store_name is not None and
-            self.store_secrets is not None and
-            "exchanged_encrypted" in self.store_secrets)
+            config.charms.charmhub_url is not None
+            and self.store_name is not None
+            and self.store_secrets is not None
+            and "exchanged_encrypted" in self.store_secrets
+        )
 
     @property
     def valid_webhook_event_types(self):
@@ -767,58 +850,86 @@ class CharmRecipe(StormBase, WebhookTargetMixin):
         # rather than in bulk.
         buildqueue_records = store.find(
             BuildQueue,
-            BuildQueue._build_farm_job_id ==
-                CharmRecipeBuild.build_farm_job_id,
-            CharmRecipeBuild.recipe == self)
+            BuildQueue._build_farm_job_id
+            == CharmRecipeBuild.build_farm_job_id,
+            CharmRecipeBuild.recipe == self,
+        )
         for buildqueue_record in buildqueue_records:
             buildqueue_record.destroySelf()
-        build_farm_job_ids = list(store.find(
-            CharmRecipeBuild.build_farm_job_id,
-            CharmRecipeBuild.recipe == self))
-        store.execute("""
+        build_farm_job_ids = list(
+            store.find(
+                CharmRecipeBuild.build_farm_job_id,
+                CharmRecipeBuild.recipe == self,
+            )
+        )
+        store.execute(
+            """
             DELETE FROM CharmFile
             USING CharmRecipeBuild
             WHERE
                 CharmFile.build = CharmRecipeBuild.id AND
                 CharmRecipeBuild.recipe = ?
-            """, (self.id,))
-        store.execute("""
+            """,
+            (self.id,),
+        )
+        store.execute(
+            """
             DELETE FROM CharmRecipeBuildJob
             USING CharmRecipeBuild
             WHERE
                 CharmRecipeBuildJob.build = CharmRecipeBuild.id AND
                 CharmRecipeBuild.recipe = ?
-            """, (self.id,))
+            """,
+            (self.id,),
+        )
         store.find(CharmRecipeBuild, CharmRecipeBuild.recipe == self).remove()
         affected_jobs = Select(
             [CharmRecipeJob.job_id],
-            And(CharmRecipeJob.job == Job.id, CharmRecipeJob.recipe == self))
+            And(CharmRecipeJob.job == Job.id, CharmRecipeJob.recipe == self),
+        )
         store.find(Job, Job.id.is_in(affected_jobs)).remove()
         getUtility(IWebhookSet).delete(self.webhooks)
         store.remove(self)
         store.find(
-            BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)).remove()
+            BuildFarmJob, BuildFarmJob.id.is_in(build_farm_job_ids)
+        ).remove()
 
 
 @implementer(ICharmRecipeSet)
 class CharmRecipeSet:
     """See `ICharmRecipeSet`."""
 
-    def new(self, registrant, owner, project, name, description=None,
-            git_ref=None, build_path=None, require_virtualized=True,
-            information_type=InformationType.PUBLIC, auto_build=False,
-            auto_build_channels=None, store_upload=False, store_name=None,
-            store_secrets=None, store_channels=None, date_created=DEFAULT):
+    def new(
+        self,
+        registrant,
+        owner,
+        project,
+        name,
+        description=None,
+        git_ref=None,
+        build_path=None,
+        require_virtualized=True,
+        information_type=InformationType.PUBLIC,
+        auto_build=False,
+        auto_build_channels=None,
+        store_upload=False,
+        store_name=None,
+        store_secrets=None,
+        store_channels=None,
+        date_created=DEFAULT,
+    ):
         """See `ICharmRecipeSet`."""
         if not registrant.inTeam(owner):
             if owner.is_team:
                 raise CharmRecipeNotOwner(
-                    "%s is not a member of %s." %
-                    (registrant.displayname, owner.displayname))
+                    "%s is not a member of %s."
+                    % (registrant.displayname, owner.displayname)
+                )
             else:
                 raise CharmRecipeNotOwner(
-                    "%s cannot create charm recipes owned by %s." %
-                    (registrant.displayname, owner.displayname))
+                    "%s cannot create charm recipes owned by %s."
+                    % (registrant.displayname, owner.displayname)
+                )
 
         if git_ref is None:
             raise NoSourceForCharmRecipe
@@ -830,27 +941,38 @@ class CharmRecipeSet:
         # IntegrityError due to exceptions being raised during object
         # creation and to ensure that everything relevant is in the Storm
         # cache.
-        if not self.isValidInformationType(
-                information_type, owner, git_ref):
+        if not self.isValidInformationType(information_type, owner, git_ref):
             raise CharmRecipePrivacyMismatch
 
         store = IMasterStore(CharmRecipe)
         recipe = CharmRecipe(
-            registrant, owner, project, name, description=description,
-            git_ref=git_ref, build_path=build_path,
+            registrant,
+            owner,
+            project,
+            name,
+            description=description,
+            git_ref=git_ref,
+            build_path=build_path,
             require_virtualized=require_virtualized,
-            information_type=information_type, auto_build=auto_build,
+            information_type=information_type,
+            auto_build=auto_build,
             auto_build_channels=auto_build_channels,
-            store_upload=store_upload, store_name=store_name,
-            store_secrets=store_secrets, store_channels=store_channels,
-            date_created=date_created)
+            store_upload=store_upload,
+            store_name=store_name,
+            store_secrets=store_secrets,
+            store_channels=store_channels,
+            date_created=date_created,
+        )
         store.add(recipe)
 
         return recipe
 
     def _getByName(self, owner, project, name):
-        return IStore(CharmRecipe).find(
-            CharmRecipe, owner=owner, project=project, name=name).one()
+        return (
+            IStore(CharmRecipe)
+            .find(CharmRecipe, owner=owner, project=project, name=name)
+            .one()
+        )
 
     def exists(self, owner, project, name):
         """See `ICharmRecipeSet`."""
@@ -863,8 +985,9 @@ class CharmRecipeSet:
             raise NoSuchCharmRecipe(name)
         return recipe
 
-    def _getRecipesFromCollection(self, collection, owner=None,
-                                  visible_by_user=None):
+    def _getRecipesFromCollection(
+        self, collection, owner=None, visible_by_user=None
+    ):
         id_column = CharmRecipe.git_repository_id
         ids = collection.getRepositoryIds()
         expressions = [id_column.is_in(ids._get_select())]
@@ -879,12 +1002,15 @@ class CharmRecipeSet:
 
     def findByPerson(self, person, visible_by_user=None):
         """See `ICharmRecipeSet`."""
+
         def _getRecipes(collection):
             collection = collection.visibleByUser(visible_by_user)
             owned = self._getRecipesFromCollection(
-                collection.ownedBy(person), visible_by_user=visible_by_user)
+                collection.ownedBy(person), visible_by_user=visible_by_user
+            )
             packaged = self._getRecipesFromCollection(
-                collection, owner=person, visible_by_user=visible_by_user)
+                collection, owner=person, visible_by_user=visible_by_user
+            )
             return owned.union(packaged)
 
         git_collection = removeSecurityProxy(getUtility(IAllGitRepositories))
@@ -893,20 +1019,28 @@ class CharmRecipeSet:
 
     def findByProject(self, project, visible_by_user=None):
         """See `ICharmRecipeSet`."""
+
         def _getRecipes(collection):
             return self._getRecipesFromCollection(
                 collection.visibleByUser(visible_by_user),
-                visible_by_user=visible_by_user)
+                visible_by_user=visible_by_user,
+            )
 
         recipes_for_project = IStore(CharmRecipe).find(
             CharmRecipe,
             CharmRecipe.project == project,
-            get_charm_recipe_privacy_filter(visible_by_user))
+            get_charm_recipe_privacy_filter(visible_by_user),
+        )
         git_collection = removeSecurityProxy(IGitCollection(project))
         return recipes_for_project.union(_getRecipes(git_collection))
 
-    def findByGitRepository(self, repository, paths=None,
-                            visible_by_user=None, check_permissions=True):
+    def findByGitRepository(
+        self,
+        repository,
+        paths=None,
+        visible_by_user=None,
+        check_permissions=True,
+    ):
         """See `ICharmRecipeSet`."""
         clauses = [CharmRecipe.git_repository == repository]
         if paths is not None:
@@ -921,22 +1055,27 @@ class CharmRecipeSet:
             CharmRecipe,
             CharmRecipe.git_repository == ref.repository,
             CharmRecipe.git_path == ref.path,
-            get_charm_recipe_privacy_filter(visible_by_user))
+            get_charm_recipe_privacy_filter(visible_by_user),
+        )
 
     def findByContext(self, context, visible_by_user=None, order_by_date=True):
         """See `ICharmRecipeSet`."""
         if IPerson.providedBy(context):
             recipes = self.findByPerson(
-                context, visible_by_user=visible_by_user)
+                context, visible_by_user=visible_by_user
+            )
         elif IProduct.providedBy(context):
             recipes = self.findByProject(
-                context, visible_by_user=visible_by_user)
+                context, visible_by_user=visible_by_user
+            )
         elif IGitRepository.providedBy(context):
             recipes = self.findByGitRepository(
-                context, visible_by_user=visible_by_user)
+                context, visible_by_user=visible_by_user
+            )
         elif IGitRef.providedBy(context):
             recipes = self.findByGitRef(
-                context, visible_by_user=visible_by_user)
+                context, visible_by_user=visible_by_user
+            )
         else:
             raise BadCharmRecipeSearchContext(context)
         if order_by_date:
@@ -974,12 +1113,14 @@ class CharmRecipeSet:
             person_ids.add(recipe.owner_id)
 
         repositories = load_related(
-            GitRepository, recipes, ["git_repository_id"])
+            GitRepository, recipes, ["git_repository_id"]
+        )
         if repositories:
             GenericGitCollection.preloadDataForRepositories(repositories)
 
         git_refs = GitRef.findByReposAndPaths(
-            [(recipe.git_repository, recipe.git_path) for recipe in recipes])
+            [(recipe.git_repository, recipe.git_path) for recipe in recipes]
+        )
         for recipe in recipes:
             git_ref = git_refs.get((recipe.git_repository, recipe.git_path))
             if git_ref is not None:
@@ -990,8 +1131,11 @@ class CharmRecipeSet:
         # aren't trigger-maintained.
         person_ids.update(repository.owner_id for repository in repositories)
 
-        list(getUtility(IPersonSet).getPrecachedPersonsFromIDs(
-            person_ids, need_validity=True))
+        list(
+            getUtility(IPersonSet).getPrecachedPersonsFromIDs(
+                person_ids, need_validity=True
+            )
+        )
 
     def getCharmcraftYaml(self, context, logger=None):
         """See `ICharmRecipeSet`."""
@@ -1012,58 +1156,68 @@ class CharmRecipeSet:
             except GitRepositoryBlobNotFound:
                 if logger is not None:
                     logger.exception(
-                        "Cannot find charmcraft.yaml in %s",
-                        source.unique_name)
+                        "Cannot find charmcraft.yaml in %s", source.unique_name
+                    )
                 raise MissingCharmcraftYaml(source.unique_name)
         except GitRepositoryScanFault as e:
             msg = "Failed to get charmcraft.yaml from %s"
             if logger is not None:
                 logger.exception(msg, source.unique_name)
             raise CannotFetchCharmcraftYaml(
-                "%s: %s" % (msg % source.unique_name, e))
+                "%s: %s" % (msg % source.unique_name, e)
+            )
 
         try:
             charmcraft_data = yaml.safe_load(blob)
         except Exception as e:
             # Don't bother logging parsing errors from user-supplied YAML.
             raise CannotParseCharmcraftYaml(
-                "Cannot parse charmcraft.yaml from %s: %s" %
-                (source.unique_name, e))
+                "Cannot parse charmcraft.yaml from %s: %s"
+                % (source.unique_name, e)
+            )
 
         if not isinstance(charmcraft_data, dict):
             raise CannotParseCharmcraftYaml(
-                "The top level of charmcraft.yaml from %s is not a mapping" %
-                source.unique_name)
+                "The top level of charmcraft.yaml from %s is not a mapping"
+                % source.unique_name
+            )
 
         return charmcraft_data
 
     @staticmethod
     def _findStaleRecipes():
         """Find recipes that need to be rebuilt."""
-        threshold_date = (
-            datetime.now(pytz.UTC) -
-            timedelta(minutes=config.charms.auto_build_frequency))
+        threshold_date = datetime.now(pytz.UTC) - timedelta(
+            minutes=config.charms.auto_build_frequency
+        )
         stale_clauses = [
             IsTrue(CharmRecipe.is_stale),
             IsTrue(CharmRecipe.auto_build),
-            ]
+        ]
         recent_clauses = [
             CharmRecipeJob.recipe_id == CharmRecipe.id,
             CharmRecipeJob.job_type == CharmRecipeJobType.REQUEST_BUILDS,
-            JSONExtract(CharmRecipeJob.metadata, "channels") == Coalesce(
-                CharmRecipe.auto_build_channels, Cast("null", "jsonb")),
+            JSONExtract(CharmRecipeJob.metadata, "channels")
+            == Coalesce(
+                CharmRecipe.auto_build_channels, Cast("null", "jsonb")
+            ),
             CharmRecipeJob.job_id == Job.id,
             # We only want recipes that haven't had an automatic build
             # requested for them recently.
             Job.date_created >= threshold_date,
-            ]
+        ]
         return IStore(CharmRecipe).find(
             CharmRecipe,
-            CharmRecipe.id.is_in(Except(
-                Select(CharmRecipe.id, where=And(*stale_clauses)),
-                Select(
-                    CharmRecipe.id,
-                    where=And(*(stale_clauses + recent_clauses))))))
+            CharmRecipe.id.is_in(
+                Except(
+                    Select(CharmRecipe.id, where=And(*stale_clauses)),
+                    Select(
+                        CharmRecipe.id,
+                        where=And(*(stale_clauses + recent_clauses)),
+                    ),
+                )
+            ),
+        )
 
     @classmethod
     def makeAutoBuilds(cls, logger=None):
@@ -1086,7 +1240,8 @@ class CharmRecipeSet:
         for recipe in recipes:
             get_property_cache(recipe)._git_ref = None
         recipes.set(
-            git_repository_id=None, git_path=None, date_last_modified=UTC_NOW)
+            git_repository_id=None, git_path=None, date_last_modified=UTC_NOW
+        )
 
     def empty_list(self):
         """See `ICharmRecipeSet`."""
@@ -1095,12 +1250,12 @@ class CharmRecipeSet:
 
 @implementer(IEncryptedContainer)
 class CharmhubSecretsEncryptedContainer(NaClEncryptedContainerBase):
-
     @property
     def public_key_bytes(self):
         if config.charms.charmhub_secrets_public_key is not None:
             return base64.b64decode(
-                config.charms.charmhub_secrets_public_key.encode())
+                config.charms.charmhub_secrets_public_key.encode()
+            )
         else:
             return None
 
@@ -1108,7 +1263,8 @@ class CharmhubSecretsEncryptedContainer(NaClEncryptedContainerBase):
     def private_key_bytes(self):
         if config.charms.charmhub_secrets_private_key is not None:
             return base64.b64decode(
-                config.charms.charmhub_secrets_private_key.encode())
+                config.charms.charmhub_secrets_private_key.encode()
+            )
         else:
             return None
 
@@ -1116,7 +1272,8 @@ class CharmhubSecretsEncryptedContainer(NaClEncryptedContainerBase):
 def get_charm_recipe_privacy_filter(user):
     """Return a Storm query filter to find charm recipes visible to `user`."""
     public_filter = CharmRecipe.information_type.is_in(
-        PUBLIC_INFORMATION_TYPES)
+        PUBLIC_INFORMATION_TYPES
+    )
 
     # XXX cjwatson 2021-06-07: Flesh this out once we have more privacy
     # infrastructure.
diff --git a/lib/lp/charms/model/charmrecipebuild.py b/lib/lp/charms/model/charmrecipebuild.py
index 3e39c63..c7f830f 100644
--- a/lib/lp/charms/model/charmrecipebuild.py
+++ b/lib/lp/charms/model/charmrecipebuild.py
@@ -6,7 +6,7 @@
 __all__ = [
     "CharmFile",
     "CharmRecipeBuild",
-    ]
+]
 
 from datetime import timedelta
 from operator import attrgetter
@@ -14,12 +14,9 @@ from operator import attrgetter
 import pytz
 import six
 from storm.databases.postgres import JSON
-from storm.expr import (
-    Column,
-    Table,
-    With,
-    )
+from storm.expr import Column, Table, With
 from storm.locals import (
+    SQL,
     And,
     Bool,
     DateTime,
@@ -27,10 +24,9 @@ from storm.locals import (
     Int,
     Reference,
     Select,
-    SQL,
     Store,
     Unicode,
-    )
+)
 from storm.store import EmptyResultSet
 from zope.component import getUtility
 from zope.interface import implementer
@@ -40,7 +36,7 @@ from lp.buildmaster.enums import (
     BuildFarmJobType,
     BuildQueueStatus,
     BuildStatus,
-    )
+)
 from lp.buildmaster.interfaces.buildfarmjob import IBuildFarmJobSource
 from lp.buildmaster.model.buildfarmjob import SpecificBuildFarmJobSourceMixin
 from lp.buildmaster.model.packagebuild import PackageBuildMixin
@@ -51,13 +47,13 @@ from lp.charms.interfaces.charmrecipebuild import (
     ICharmFile,
     ICharmRecipeBuild,
     ICharmRecipeBuildSet,
-    )
+)
 from lp.charms.interfaces.charmrecipebuildjob import ICharmhubUploadJobSource
 from lp.charms.mail.charmrecipebuild import CharmRecipeBuildMailer
 from lp.charms.model.charmrecipebuildjob import (
     CharmRecipeBuildJob,
     CharmRecipeBuildJobType,
-    )
+)
 from lp.registry.interfaces.pocket import PackagePublishingPocket
 from lp.registry.interfaces.series import SeriesStatus
 from lp.registry.model.distribution import Distribution
@@ -68,21 +64,12 @@ from lp.services.database.bulk import load_related
 from lp.services.database.constants import DEFAULT
 from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
-    IMasterStore,
-    IStore,
-    )
+from lp.services.database.interfaces import IMasterStore, IStore
 from lp.services.database.stormbase import StormBase
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.model.job import Job
-from lp.services.librarian.model import (
-    LibraryFileAlias,
-    LibraryFileContent,
-    )
-from lp.services.propertycache import (
-    cachedproperty,
-    get_property_cache,
-    )
+from lp.services.librarian.model import LibraryFileAlias, LibraryFileContent
+from lp.services.propertycache import cachedproperty, get_property_cache
 from lp.services.webapp.snapshot import notify_modified
 from lp.soyuz.model.distroarchseries import DistroArchSeries
 
@@ -107,7 +94,8 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
 
     distro_arch_series_id = Int(name="distro_arch_series", allow_none=False)
     distro_arch_series = Reference(
-        distro_arch_series_id, "DistroArchSeries.id")
+        distro_arch_series_id, "DistroArchSeries.id"
+    )
 
     channels = JSON("channels", allow_none=True)
 
@@ -117,13 +105,17 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
     virtualized = Bool(name="virtualized", allow_none=False)
 
     date_created = DateTime(
-        name="date_created", tzinfo=pytz.UTC, allow_none=False)
+        name="date_created", tzinfo=pytz.UTC, allow_none=False
+    )
     date_started = DateTime(
-        name="date_started", tzinfo=pytz.UTC, allow_none=True)
+        name="date_started", tzinfo=pytz.UTC, allow_none=True
+    )
     date_finished = DateTime(
-        name="date_finished", tzinfo=pytz.UTC, allow_none=True)
+        name="date_finished", tzinfo=pytz.UTC, allow_none=True
+    )
     date_first_dispatched = DateTime(
-        name="date_first_dispatched", tzinfo=pytz.UTC, allow_none=True)
+        name="date_first_dispatched", tzinfo=pytz.UTC, allow_none=True
+    )
 
     builder_id = Int(name="builder", allow_none=True)
     builder = Reference(builder_id, "Builder.id")
@@ -147,9 +139,18 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
 
     store_upload_metadata = JSON("store_upload_json_data", allow_none=True)
 
-    def __init__(self, build_farm_job, build_request, recipe,
-                 distro_arch_series, processor, virtualized, channels=None,
-                 store_upload_metadata=None, date_created=DEFAULT):
+    def __init__(
+        self,
+        build_farm_job,
+        build_request,
+        recipe,
+        distro_arch_series,
+        processor,
+        virtualized,
+        channels=None,
+        store_upload_metadata=None,
+        date_created=DEFAULT,
+    ):
         """Construct a `CharmRecipeBuild`."""
         requester = build_request.requester
         super().__init__()
@@ -176,14 +177,20 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
 
     def __repr__(self):
         return "<CharmRecipeBuild ~%s/%s/+charm/%s/+build/%d>" % (
-            self.recipe.owner.name, self.recipe.project.name, self.recipe.name,
-            self.id)
+            self.recipe.owner.name,
+            self.recipe.project.name,
+            self.recipe.name,
+            self.id,
+        )
 
     @property
     def title(self):
         return "%s build of /~%s/%s/+charm/%s" % (
-            self.distro_arch_series.architecturetag, self.recipe.owner.name,
-            self.recipe.project.name, self.recipe.name)
+            self.distro_arch_series.architecturetag,
+            self.recipe.owner.name,
+            self.recipe.project.name,
+            self.recipe.name,
+        )
 
     @property
     def distribution(self):
@@ -240,7 +247,8 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
             (CharmRecipeBuild.date_started, CharmRecipeBuild.date_finished),
             CharmRecipeBuild.recipe == self.recipe,
             CharmRecipeBuild.processor == self.processor,
-            CharmRecipeBuild.status == BuildStatus.FULLYBUILT)
+            CharmRecipeBuild.status == BuildStatus.FULLYBUILT,
+        )
         result.order_by(Desc(CharmRecipeBuild.date_finished))
         durations = [row[1] - row[0] for row in result[:9]]
         if len(durations) == 0:
@@ -293,15 +301,17 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
         jobs = Store.of(self).find(
             CharmRecipeBuildJob,
             CharmRecipeBuildJob.build == self,
-            CharmRecipeBuildJob.job_type ==
-                CharmRecipeBuildJobType.CHARMHUB_UPLOAD)
+            CharmRecipeBuildJob.job_type
+            == CharmRecipeBuildJobType.CHARMHUB_UPLOAD,
+        )
         jobs.order_by(Desc(CharmRecipeBuildJob.job_id))
 
         def preload_jobs(rows):
             load_related(Job, rows, ["job_id"])
 
         return DecoratedResultSet(
-            jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs)
+            jobs, lambda job: job.makeDerived(), pre_iter_hook=preload_jobs
+        )
 
     @cachedproperty
     def last_store_upload_job(self):
@@ -337,19 +347,27 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
         if not self.recipe.can_upload_to_store:
             raise CannotScheduleStoreUpload(
                 "Cannot upload this charm to Charmhub because it is not "
-                "properly configured.")
+                "properly configured."
+            )
         if not self.was_built or self.getFiles().is_empty():
             raise CannotScheduleStoreUpload(
-                "Cannot upload this charm because it has no files.")
-        if (self.store_upload_status ==
-                CharmRecipeBuildStoreUploadStatus.PENDING):
+                "Cannot upload this charm because it has no files."
+            )
+        if (
+            self.store_upload_status
+            == CharmRecipeBuildStoreUploadStatus.PENDING
+        ):
             raise CannotScheduleStoreUpload(
-                "An upload of this charm is already in progress.")
-        elif (self.store_upload_status ==
-                CharmRecipeBuildStoreUploadStatus.UPLOADED):
+                "An upload of this charm is already in progress."
+            )
+        elif (
+            self.store_upload_status
+            == CharmRecipeBuildStoreUploadStatus.UPLOADED
+        ):
             raise CannotScheduleStoreUpload(
                 "Cannot upload this charm because it has already been "
-                "uploaded.")
+                "uploaded."
+            )
         getUtility(ICharmhubUploadJobSource).create(self)
 
     def getFiles(self):
@@ -358,7 +376,8 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
             (CharmFile, LibraryFileAlias, LibraryFileContent),
             CharmFile.build == self.id,
             LibraryFileAlias.id == CharmFile.library_file_id,
-            LibraryFileContent.id == LibraryFileAlias.contentID)
+            LibraryFileContent.id == LibraryFileAlias.contentID,
+        )
         return result.order_by([LibraryFileAlias.filename, CharmFile.id])
 
     def getFileByName(self, filename):
@@ -368,11 +387,16 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
         elif filename.endswith("_log.txt"):
             file_object = self.upload_log
         else:
-            file_object = Store.of(self).find(
-                LibraryFileAlias,
-                CharmFile.build == self.id,
-                LibraryFileAlias.id == CharmFile.library_file_id,
-                LibraryFileAlias.filename == filename).one()
+            file_object = (
+                Store.of(self)
+                .find(
+                    LibraryFileAlias,
+                    CharmFile.build == self.id,
+                    LibraryFileAlias.id == CharmFile.library_file_id,
+                    LibraryFileAlias.filename == filename,
+                )
+                .one()
+            )
 
         if file_object is not None and file_object.filename == filename:
             return file_object
@@ -389,18 +413,28 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
         """See `IPackageBuild`."""
         return not self.getFiles().is_empty()
 
-    def updateStatus(self, status, builder=None, worker_status=None,
-                     date_started=None, date_finished=None,
-                     force_invalid_transition=False):
+    def updateStatus(
+        self,
+        status,
+        builder=None,
+        worker_status=None,
+        date_started=None,
+        date_finished=None,
+        force_invalid_transition=False,
+    ):
         """See `IBuildFarmJob`."""
         edited_fields = set()
         with notify_modified(
-                self, edited_fields,
-                snapshot_names=("status", "revision_id")) as previous_obj:
+            self, edited_fields, snapshot_names=("status", "revision_id")
+        ) as previous_obj:
             super().updateStatus(
-                status, builder=builder, worker_status=worker_status,
-                date_started=date_started, date_finished=date_finished,
-                force_invalid_transition=force_invalid_transition)
+                status,
+                builder=builder,
+                worker_status=worker_status,
+                date_started=date_started,
+                date_finished=date_finished,
+                force_invalid_transition=force_invalid_transition,
+            )
             if self.status != previous_obj.status:
                 edited_fields.add("status")
             if worker_status is not None:
@@ -427,20 +461,35 @@ class CharmRecipeBuild(PackageBuildMixin, StormBase):
 class CharmRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
     """See `ICharmRecipeBuildSet`."""
 
-    def new(self, build_request, recipe, distro_arch_series, channels=None,
-            store_upload_metadata=None, date_created=DEFAULT):
+    def new(
+        self,
+        build_request,
+        recipe,
+        distro_arch_series,
+        channels=None,
+        store_upload_metadata=None,
+        date_created=DEFAULT,
+    ):
         """See `ICharmRecipeBuildSet`."""
         store = IMasterStore(CharmRecipeBuild)
         build_farm_job = getUtility(IBuildFarmJobSource).new(
-            CharmRecipeBuild.job_type, BuildStatus.NEEDSBUILD, date_created)
+            CharmRecipeBuild.job_type, BuildStatus.NEEDSBUILD, date_created
+        )
         virtualized = (
             not distro_arch_series.processor.supports_nonvirtualized
-            or recipe.require_virtualized)
+            or recipe.require_virtualized
+        )
         build = CharmRecipeBuild(
-            build_farm_job, build_request, recipe, distro_arch_series,
-            distro_arch_series.processor, virtualized, channels=channels,
+            build_farm_job,
+            build_request,
+            recipe,
+            distro_arch_series,
+            distro_arch_series.processor,
+            virtualized,
+            channels=channels,
             store_upload_metadata=store_upload_metadata,
-            date_created=date_created)
+            date_created=date_created,
+        )
         store.add(build)
         return build
 
@@ -451,45 +500,65 @@ class CharmRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
 
     def getByBuildFarmJob(self, build_farm_job):
         """See `ISpecificBuildFarmJobSource`."""
-        return Store.of(build_farm_job).find(
-            CharmRecipeBuild, build_farm_job_id=build_farm_job.id).one()
+        return (
+            Store.of(build_farm_job)
+            .find(CharmRecipeBuild, build_farm_job_id=build_farm_job.id)
+            .one()
+        )
 
     def preloadBuildsData(self, builds):
         # Circular import.
         from lp.charms.model.charmrecipe import CharmRecipe
+
         load_related(Person, builds, ["requester_id"])
         lfas = load_related(LibraryFileAlias, builds, ["log_id"])
         load_related(LibraryFileContent, lfas, ["contentID"])
         distroarchserieses = load_related(
-            DistroArchSeries, builds, ["distro_arch_series_id"])
+            DistroArchSeries, builds, ["distro_arch_series_id"]
+        )
         distroserieses = load_related(
-            DistroSeries, distroarchserieses, ["distroseriesID"])
+            DistroSeries, distroarchserieses, ["distroseriesID"]
+        )
         load_related(Distribution, distroserieses, ["distributionID"])
         recipes = load_related(CharmRecipe, builds, ["recipe_id"])
         getUtility(ICharmRecipeSet).preloadDataForRecipes(recipes)
         build_ids = set(map(attrgetter("id"), builds))
-        latest_jobs_cte = With("LatestJobs", Select(
-            (CharmRecipeBuildJob.job_id,
-             SQL(
-                 "rank() OVER "
-                 "(PARTITION BY build ORDER BY job DESC) AS rank")),
-            tables=CharmRecipeBuildJob,
-            where=And(
-                CharmRecipeBuildJob.build_id.is_in(build_ids),
-                CharmRecipeBuildJob.job_type ==
-                    CharmRecipeBuildJobType.CHARMHUB_UPLOAD)))
+        latest_jobs_cte = With(
+            "LatestJobs",
+            Select(
+                (
+                    CharmRecipeBuildJob.job_id,
+                    SQL(
+                        "rank() OVER "
+                        "(PARTITION BY build ORDER BY job DESC) AS rank"
+                    ),
+                ),
+                tables=CharmRecipeBuildJob,
+                where=And(
+                    CharmRecipeBuildJob.build_id.is_in(build_ids),
+                    CharmRecipeBuildJob.job_type
+                    == CharmRecipeBuildJobType.CHARMHUB_UPLOAD,
+                ),
+            ),
+        )
         LatestJobs = Table("LatestJobs")
-        crbjs = list(IStore(CharmRecipeBuildJob).with_(latest_jobs_cte).using(
-            CharmRecipeBuildJob, LatestJobs).find(
+        crbjs = list(
+            IStore(CharmRecipeBuildJob)
+            .with_(latest_jobs_cte)
+            .using(CharmRecipeBuildJob, LatestJobs)
+            .find(
                 CharmRecipeBuildJob,
                 CharmRecipeBuildJob.job_id == Column("job", LatestJobs),
-                Column("rank", LatestJobs) == 1))
+                Column("rank", LatestJobs) == 1,
+            )
+        )
         crbj_map = {}
         for crbj in crbjs:
             crbj_map[crbj.build] = crbj.makeDerived()
         for build in builds:
-            get_property_cache(build).last_store_upload_job = (
-                crbj_map.get(build))
+            get_property_cache(build).last_store_upload_job = crbj_map.get(
+                build
+            )
         load_related(Job, crbjs, ["job_id"])
 
     def getByBuildFarmJobs(self, build_farm_jobs):
@@ -497,8 +566,11 @@ class CharmRecipeBuildSet(SpecificBuildFarmJobSourceMixin):
         if len(build_farm_jobs) == 0:
             return EmptyResultSet()
         rows = Store.of(build_farm_jobs[0]).find(
-            CharmRecipeBuild, CharmRecipeBuild.build_farm_job_id.is_in(
-                bfj.id for bfj in build_farm_jobs))
+            CharmRecipeBuild,
+            CharmRecipeBuild.build_farm_job_id.is_in(
+                bfj.id for bfj in build_farm_jobs
+            ),
+        )
         return DecoratedResultSet(rows, pre_iter_hook=self.preloadBuildsData)
 
 
diff --git a/lib/lp/charms/model/charmrecipebuildbehaviour.py b/lib/lp/charms/model/charmrecipebuildbehaviour.py
index 7c8dabc..fffe7cf 100644
--- a/lib/lp/charms/model/charmrecipebuildbehaviour.py
+++ b/lib/lp/charms/model/charmrecipebuildbehaviour.py
@@ -8,7 +8,7 @@ Dispatches charm recipe build jobs to build-farm workers.
 
 __all__ = [
     "CharmRecipeBuildBehaviour",
-    ]
+]
 
 from twisted.internet import defer
 from zope.component import adapter
@@ -20,15 +20,13 @@ from lp.buildmaster.enums import BuildBaseImageType
 from lp.buildmaster.interfaces.builder import CannotBuild
 from lp.buildmaster.interfaces.buildfarmjobbehaviour import (
     IBuildFarmJobBehaviour,
-    )
+)
 from lp.buildmaster.model.buildfarmjobbehaviour import (
     BuildFarmJobBehaviourBase,
-    )
+)
 from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
 from lp.registry.interfaces.series import SeriesStatus
-from lp.soyuz.adapters.archivedependencies import (
-    get_sources_list_for_building,
-    )
+from lp.soyuz.adapters.archivedependencies import get_sources_list_for_building
 
 
 @adapter(ICharmRecipeBuild)
@@ -45,9 +43,12 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
         # Examples:
         #   buildlog_charm_ubuntu_wily_amd64_name_FULLYBUILT.txt
         return "buildlog_charm_%s_%s_%s_%s_%s.txt" % (
-            das.distroseries.distribution.name, das.distroseries.name,
-            das.architecturetag, self.build.recipe.name,
-            self.build.status.name)
+            das.distroseries.distribution.name,
+            das.distroseries.name,
+            das.architecturetag,
+            self.build.recipe.name,
+            self.build.status.name,
+        )
 
     def verifyBuildRequest(self, logger):
         """Assert some pre-build checks.
@@ -59,12 +60,14 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
         build = self.build
         if build.virtualized and not self._builder.virtualized:
             raise AssertionError(
-                "Attempt to build virtual item on a non-virtual builder.")
+                "Attempt to build virtual item on a non-virtual builder."
+            )
 
         chroot = build.distro_arch_series.getChroot()
         if chroot is None:
             raise CannotBuild(
-                "Missing chroot for %s" % build.distro_arch_series.displayname)
+                "Missing chroot for %s" % build.distro_arch_series.displayname
+            )
 
     @defer.inlineCallbacks
     def extraBuildArgs(self, logger=None):
@@ -79,9 +82,12 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
         # We have to remove the security proxy that Zope applies to this
         # dict, since otherwise we'll be unable to serialise it to XML-RPC.
         args["channels"] = removeSecurityProxy(channels)
-        args["archives"], args["trusted_keys"] = (
-            yield get_sources_list_for_building(
-                self, build.distro_arch_series, None, logger=logger))
+        (
+            args["archives"],
+            args["trusted_keys"],
+        ) = yield get_sources_list_for_building(
+            self, build.distro_arch_series, None, logger=logger
+        )
         if build.recipe.build_path is not None:
             args["build_path"] = build.recipe.build_path
         if build.recipe.git_ref is not None:
@@ -93,9 +99,13 @@ class CharmRecipeBuildBehaviour(BuilderProxyMixin, BuildFarmJobBehaviourBase):
                 args["git_path"] = build.recipe.git_ref.name
         else:
             raise CannotBuild(
-                "Source repository for ~%s/%s/+charm/%s has been deleted." % (
-                    build.recipe.owner.name, build.recipe.project.name,
-                    build.recipe.name))
+                "Source repository for ~%s/%s/+charm/%s has been deleted."
+                % (
+                    build.recipe.owner.name,
+                    build.recipe.project.name,
+                    build.recipe.name,
+                )
+            )
         args["private"] = build.is_private
         return args
 
diff --git a/lib/lp/charms/model/charmrecipebuildjob.py b/lib/lp/charms/model/charmrecipebuildjob.py
index d3fdd5a..41638e9 100644
--- a/lib/lp/charms/model/charmrecipebuildjob.py
+++ b/lib/lp/charms/model/charmrecipebuildjob.py
@@ -7,26 +7,17 @@ __all__ = [
     "CharmhubUploadJob",
     "CharmRecipeBuildJob",
     "CharmRecipeBuildJobType",
-    ]
+]
 
 from datetime import timedelta
 
+import transaction
 from lazr.delegates import delegate_to
-from lazr.enum import (
-    DBEnumeratedType,
-    DBItem,
-    )
+from lazr.enum import DBEnumeratedType, DBItem
 from storm.databases.postgres import JSON
-from storm.locals import (
-    Int,
-    Reference,
-    )
-import transaction
+from storm.locals import Int, Reference
 from zope.component import getUtility
-from zope.interface import (
-    implementer,
-    provider,
-    )
+from zope.interface import implementer, provider
 
 from lp.app.errors import NotFoundError
 from lp.charms.interfaces.charmhubclient import (
@@ -38,24 +29,18 @@ from lp.charms.interfaces.charmhubclient import (
     UnauthorizedUploadResponse,
     UploadFailedResponse,
     UploadNotReviewedYetResponse,
-    )
+)
 from lp.charms.interfaces.charmrecipebuildjob import (
     ICharmhubUploadJob,
     ICharmhubUploadJobSource,
     ICharmRecipeBuildJob,
-    )
+)
 from lp.charms.mail.charmrecipebuild import CharmRecipeBuildMailer
 from lp.services.config import config
 from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
-    IMasterStore,
-    IStore,
-    )
+from lp.services.database.interfaces import IMasterStore, IStore
 from lp.services.database.stormbase import StormBase
-from lp.services.job.model.job import (
-    EnumeratedSubclass,
-    Job,
-    )
+from lp.services.job.model.job import EnumeratedSubclass, Job
 from lp.services.job.runner import BaseRunnableJob
 from lp.services.propertycache import get_property_cache
 from lp.services.webapp.snapshot import notify_modified
@@ -64,11 +49,14 @@ from lp.services.webapp.snapshot import notify_modified
 class CharmRecipeBuildJobType(DBEnumeratedType):
     """Values that `ICharmRecipeBuildJob.job_type` can take."""
 
-    CHARMHUB_UPLOAD = DBItem(0, """
+    CHARMHUB_UPLOAD = DBItem(
+        0,
+        """
         Charmhub upload
 
         This job uploads a charm recipe build to Charmhub.
-        """)
+        """,
+    )
 
 
 @implementer(ICharmRecipeBuildJob)
@@ -110,8 +98,8 @@ class CharmRecipeBuildJob(StormBase):
 
 @delegate_to(ICharmRecipeBuildJob)
 class CharmRecipeBuildJobDerived(
-        BaseRunnableJob, metaclass=EnumeratedSubclass):
-
+    BaseRunnableJob, metaclass=EnumeratedSubclass
+):
     def __init__(self, charm_recipe_build_job):
         self.context = charm_recipe_build_job
 
@@ -119,8 +107,12 @@ class CharmRecipeBuildJobDerived(
         """An informative representation of the job."""
         recipe = self.build.recipe
         return "<%s for ~%s/%s/+charm/%s/+build/%d>" % (
-            self.__class__.__name__, recipe.owner.name, recipe.project.name,
-            recipe.name, self.build.id)
+            self.__class__.__name__,
+            recipe.owner.name,
+            recipe.project.name,
+            recipe.name,
+            self.build.id,
+        )
 
     @classmethod
     def get(cls, job_id):
@@ -132,11 +124,13 @@ class CharmRecipeBuildJobDerived(
             or its `job_type` does not match the desired subclass.
         """
         charm_recipe_build_job = IStore(CharmRecipeBuildJob).get(
-            CharmRecipeBuildJob, job_id)
+            CharmRecipeBuildJob, job_id
+        )
         if charm_recipe_build_job.job_type != cls.class_job_type:
             raise NotFoundError(
-                "No object found with id %d and type %s" %
-                (job_id, cls.class_job_type.title))
+                "No object found with id %d and type %s"
+                % (job_id, cls.class_job_type.title)
+            )
         return cls(charm_recipe_build_job)
 
     @classmethod
@@ -146,21 +140,24 @@ class CharmRecipeBuildJobDerived(
             CharmRecipeBuildJob,
             CharmRecipeBuildJob.job_type == cls.class_job_type,
             CharmRecipeBuildJob.job == Job.id,
-            Job.id.is_in(Job.ready_jobs))
+            Job.id.is_in(Job.ready_jobs),
+        )
         return (cls(job) for job in jobs)
 
     def getOopsVars(self):
         """See `IRunnableJob`."""
         oops_vars = super().getOopsVars()
         recipe = self.context.build.recipe
-        oops_vars.extend([
-            ("job_id", self.context.job.id),
-            ("job_type", self.context.job_type.title),
-            ("build_id", self.context.build.id),
-            ("recipe_owner_name", recipe.owner.name),
-            ("recipe_project_name", recipe.project.name),
-            ("recipe_name", recipe.name),
-            ])
+        oops_vars.extend(
+            [
+                ("job_id", self.context.job.id),
+                ("job_type", self.context.job_type.title),
+                ("build_id", self.context.build.id),
+                ("recipe_owner_name", recipe.owner.name),
+                ("recipe_project_name", recipe.project.name),
+                ("recipe_name", recipe.name),
+            ]
+        )
         return oops_vars
 
 
@@ -179,7 +176,7 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
         UnauthorizedUploadResponse,
         ReviewFailedResponse,
         ReleaseFailedResponse,
-        )
+    )
 
     retry_error_types = (UploadNotReviewedYetResponse, RetryableCharmhubError)
     max_retries = 30
@@ -192,7 +189,8 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
         edited_fields = set()
         with notify_modified(build, edited_fields) as before_modification:
             charm_recipe_build_job = CharmRecipeBuildJob(
-                build, cls.class_job_type, {})
+                build, cls.class_job_type, {}
+            )
             job = cls(charm_recipe_build_job)
             job.celeryRunOnCommit()
             del get_property_cache(build).last_store_upload_job
@@ -267,12 +265,14 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
     # Ideally we'd just override Job._set_status or similar, but
     # lazr.delegates makes that difficult, so we use this to override all
     # the individual Job lifecycle methods instead.
-    def _do_lifecycle(self, method_name, manage_transaction=False,
-                      *args, **kwargs):
+    def _do_lifecycle(
+        self, method_name, manage_transaction=False, *args, **kwargs
+    ):
         edited_fields = set()
         with notify_modified(self.build, edited_fields) as before_modification:
             getattr(super(), method_name)(
-                *args, manage_transaction=manage_transaction, **kwargs)
+                *args, manage_transaction=manage_transaction, **kwargs
+            )
             upload_status = self.build.store_upload_status
             if upload_status != before_modification.store_upload_status:
                 edited_fields.add("store_upload_status")
@@ -324,9 +324,13 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
         try:
             try:
                 charm_lfa = next(
-                    (lfa for _, lfa, _ in self.build.getFiles()
-                     if lfa.filename.endswith(".charm")),
-                    None)
+                    (
+                        lfa
+                        for _, lfa, _ in self.build.getFiles()
+                        if lfa.filename.endswith(".charm")
+                    ),
+                    None,
+                )
                 if charm_lfa is None:
                     # Nothing to do.
                     self.error_message = None
@@ -341,11 +345,13 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
                     self.attempt_count = 1
                 if self.store_revision is None:
                     self.store_revision = client.checkStatus(
-                        self.build, self.status_url)
+                        self.build, self.status_url
+                    )
                     if self.store_revision is None:
                         raise AssertionError(
                             "checkStatus returned successfully but with no "
-                            "revision")
+                            "revision"
+                        )
                     # We made progress, so reset attempt_count.
                     self.attempt_count = 1
                 if self.build.recipe.store_channels:
@@ -354,21 +360,27 @@ class CharmhubUploadJob(CharmRecipeBuildJobDerived):
             except self.retry_error_types:
                 raise
             except Exception as e:
-                if (isinstance(e, CharmhubError) and e.can_retry and
-                        self.attempt_count <= self.max_retries):
+                if (
+                    isinstance(e, CharmhubError)
+                    and e.can_retry
+                    and self.attempt_count <= self.max_retries
+                ):
                     raise RetryableCharmhubError(e.args[0], detail=e.detail)
                 self.error_message = str(e)
                 self.error_detail = getattr(e, "detail", None)
                 mailer_factory = None
                 if isinstance(e, UnauthorizedUploadResponse):
                     mailer_factory = (
-                        CharmRecipeBuildMailer.forUnauthorizedUpload)
+                        CharmRecipeBuildMailer.forUnauthorizedUpload
+                    )
                 elif isinstance(e, UploadFailedResponse):
                     mailer_factory = CharmRecipeBuildMailer.forUploadFailure
                 elif isinstance(
-                        e, (BadReviewStatusResponse, ReviewFailedResponse)):
+                    e, (BadReviewStatusResponse, ReviewFailedResponse)
+                ):
                     mailer_factory = (
-                        CharmRecipeBuildMailer.forUploadReviewFailure)
+                        CharmRecipeBuildMailer.forUploadReviewFailure
+                    )
                 elif isinstance(e, ReleaseFailedResponse):
                     mailer_factory = CharmRecipeBuildMailer.forReleaseFailure
                 if mailer_factory is not None:
diff --git a/lib/lp/charms/model/charmrecipejob.py b/lib/lp/charms/model/charmrecipejob.py
index 61a4818..a54e0c5 100644
--- a/lib/lp/charms/model/charmrecipejob.py
+++ b/lib/lp/charms/model/charmrecipejob.py
@@ -7,53 +7,37 @@ __all__ = [
     "CharmRecipeJob",
     "CharmRecipeJobType",
     "CharmRecipeRequestBuildsJob",
-    ]
+]
 
+import transaction
 from lazr.delegates import delegate_to
-from lazr.enum import (
-    DBEnumeratedType,
-    DBItem,
-    )
+from lazr.enum import DBEnumeratedType, DBItem
 from storm.databases.postgres import JSON
-from storm.locals import (
-    Desc,
-    Int,
-    Reference,
-    )
+from storm.locals import Desc, Int, Reference
 from storm.store import EmptyResultSet
-import transaction
 from zope.component import getUtility
-from zope.interface import (
-    implementer,
-    provider,
-    )
+from zope.interface import implementer, provider
 
 from lp.app.errors import NotFoundError
 from lp.charms.interfaces.charmrecipe import (
     CannotFetchCharmcraftYaml,
     CannotParseCharmcraftYaml,
     MissingCharmcraftYaml,
-    )
+)
 from lp.charms.interfaces.charmrecipejob import (
     ICharmRecipeJob,
     ICharmRecipeRequestBuildsJob,
     ICharmRecipeRequestBuildsJobSource,
-    )
+)
 from lp.charms.model.charmrecipebuild import CharmRecipeBuild
 from lp.registry.interfaces.person import IPersonSet
 from lp.services.config import config
 from lp.services.database.bulk import load_related
 from lp.services.database.decoratedresultset import DecoratedResultSet
 from lp.services.database.enumcol import DBEnum
-from lp.services.database.interfaces import (
-    IMasterStore,
-    IStore,
-    )
+from lp.services.database.interfaces import IMasterStore, IStore
 from lp.services.database.stormbase import StormBase
-from lp.services.job.model.job import (
-    EnumeratedSubclass,
-    Job,
-    )
+from lp.services.job.model.job import EnumeratedSubclass, Job
 from lp.services.job.runner import BaseRunnableJob
 from lp.services.mail.sendmail import format_address_for_person
 from lp.services.propertycache import cachedproperty
@@ -63,11 +47,14 @@ from lp.services.scripts import log
 class CharmRecipeJobType(DBEnumeratedType):
     """Values that `ICharmRecipeJob.job_type` can take."""
 
-    REQUEST_BUILDS = DBItem(0, """
+    REQUEST_BUILDS = DBItem(
+        0,
+        """
         Request builds
 
         This job requests builds of a charm recipe.
-        """)
+        """,
+    )
 
 
 @implementer(ICharmRecipeJob)
@@ -83,7 +70,8 @@ class CharmRecipeJob(StormBase):
     recipe = Reference(recipe_id, "CharmRecipe.id")
 
     job_type = DBEnum(
-        name="job_type", enum=CharmRecipeJobType, allow_none=False)
+        name="job_type", enum=CharmRecipeJobType, allow_none=False
+    )
 
     metadata = JSON("json_data", allow_none=False)
 
@@ -110,15 +98,17 @@ class CharmRecipeJob(StormBase):
 
 @delegate_to(ICharmRecipeJob)
 class CharmRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
-
     def __init__(self, recipe_job):
         self.context = recipe_job
 
     def __repr__(self):
         """An informative representation of the job."""
         return "<%s for ~%s/%s/+charm/%s>" % (
-            self.__class__.__name__, self.recipe.owner.name,
-            self.recipe.project.name, self.recipe.name)
+            self.__class__.__name__,
+            self.recipe.owner.name,
+            self.recipe.project.name,
+            self.recipe.name,
+        )
 
     @classmethod
     def get(cls, job_id):
@@ -132,8 +122,9 @@ class CharmRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
         recipe_job = IStore(CharmRecipeJob).get(CharmRecipeJob, job_id)
         if recipe_job.job_type != cls.class_job_type:
             raise NotFoundError(
-                "No object found with id %d and type %s" %
-                (job_id, cls.class_job_type.title))
+                "No object found with id %d and type %s"
+                % (job_id, cls.class_job_type.title)
+            )
         return cls(recipe_job)
 
     @classmethod
@@ -143,19 +134,22 @@ class CharmRecipeJobDerived(BaseRunnableJob, metaclass=EnumeratedSubclass):
             CharmRecipeJob,
             CharmRecipeJob.job_type == cls.class_job_type,
             CharmRecipeJob.job == Job.id,
-            Job.id.is_in(Job.ready_jobs))
+            Job.id.is_in(Job.ready_jobs),
+        )
         return (cls(job) for job in jobs)
 
     def getOopsVars(self):
         """See `IRunnableJob`."""
         oops_vars = super().getOopsVars()
-        oops_vars.extend([
-            ("job_id", self.context.job.id),
-            ("job_type", self.context.job_type.title),
-            ("recipe_owner_name", self.context.recipe.owner.name),
-            ("recipe_project_name", self.context.recipe.project.name),
-            ("recipe_name", self.context.recipe.name),
-            ])
+        oops_vars.extend(
+            [
+                ("job_id", self.context.job.id),
+                ("job_type", self.context.job_type.title),
+                ("recipe_owner_name", self.context.recipe.owner.name),
+                ("recipe_project_name", self.context.recipe.project.name),
+                ("recipe_name", self.context.recipe.name),
+            ]
+        )
         return oops_vars
 
 
@@ -169,7 +163,7 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
     user_error_types = (
         CannotParseCharmcraftYaml,
         MissingCharmcraftYaml,
-        )
+    )
     retry_error_types = (CannotFetchCharmcraftYaml,)
 
     max_retries = 5
@@ -185,8 +179,9 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
             # Really a set or None, but sets aren't directly
             # JSON-serialisable.
             "architectures": (
-                list(architectures) if architectures is not None else None),
-            }
+                list(architectures) if architectures is not None else None
+            ),
+        }
         recipe_job = CharmRecipeJob(recipe, cls.class_job_type, metadata)
         job = cls(recipe_job)
         job.celeryRunOnCommit()
@@ -198,36 +193,49 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
         clauses = [
             CharmRecipeJob.recipe == recipe,
             CharmRecipeJob.job_type == cls.class_job_type,
-            ]
+        ]
         if statuses is not None:
-            clauses.extend([
-                CharmRecipeJob.job == Job.id,
-                Job._status.is_in(statuses),
-                ])
+            clauses.extend(
+                [
+                    CharmRecipeJob.job == Job.id,
+                    Job._status.is_in(statuses),
+                ]
+            )
         if job_ids is not None:
             clauses.append(CharmRecipeJob.job_id.is_in(job_ids))
-        recipe_jobs = IStore(CharmRecipeJob).find(
-            CharmRecipeJob, *clauses).order_by(Desc(CharmRecipeJob.job_id))
+        recipe_jobs = (
+            IStore(CharmRecipeJob)
+            .find(CharmRecipeJob, *clauses)
+            .order_by(Desc(CharmRecipeJob.job_id))
+        )
 
         def preload_jobs(rows):
             load_related(Job, rows, ["job_id"])
 
         return DecoratedResultSet(
-            recipe_jobs, lambda recipe_job: cls(recipe_job),
-            pre_iter_hook=preload_jobs)
+            recipe_jobs,
+            lambda recipe_job: cls(recipe_job),
+            pre_iter_hook=preload_jobs,
+        )
 
     @classmethod
     def getByRecipeAndID(cls, recipe, job_id):
         """See `ICharmRecipeRequestBuildsJobSource`."""
-        recipe_job = IStore(CharmRecipeJob).find(
-            CharmRecipeJob,
-            CharmRecipeJob.job_id == job_id,
-            CharmRecipeJob.recipe == recipe,
-            CharmRecipeJob.job_type == cls.class_job_type).one()
+        recipe_job = (
+            IStore(CharmRecipeJob)
+            .find(
+                CharmRecipeJob,
+                CharmRecipeJob.job_id == job_id,
+                CharmRecipeJob.recipe == recipe,
+                CharmRecipeJob.job_type == cls.class_job_type,
+            )
+            .one()
+        )
         if recipe_job is None:
             raise NotFoundError(
-                "No REQUEST_BUILDS job with ID %d found for %r" %
-                (job_id, recipe))
+                "No REQUEST_BUILDS job with ID %d found for %r"
+                % (job_id, recipe)
+            )
         return cls(recipe_job)
 
     def getOperationDescription(self):
@@ -286,7 +294,8 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
         build_ids = self.metadata.get("builds")
         if build_ids:
             return IStore(CharmRecipeBuild).find(
-                CharmRecipeBuild, CharmRecipeBuild.id.is_in(build_ids))
+                CharmRecipeBuild, CharmRecipeBuild.id.is_in(build_ids)
+            )
         else:
             return EmptyResultSet()
 
@@ -300,12 +309,16 @@ class CharmRecipeRequestBuildsJob(CharmRecipeJobDerived):
         requester = self.requester
         if requester is None:
             log.info(
-                "Skipping %r because the requester has been deleted." % self)
+                "Skipping %r because the requester has been deleted." % self
+            )
             return
         try:
             self.builds = self.recipe.requestBuildsFromJob(
-                self.build_request, channels=self.channels,
-                architectures=self.architectures, logger=log)
+                self.build_request,
+                channels=self.channels,
+                architectures=self.architectures,
+                logger=log,
+            )
             self.error_message = None
         except Exception as e:
             self.error_message = str(e)
diff --git a/lib/lp/charms/subscribers/charmrecipebuild.py b/lib/lp/charms/subscribers/charmrecipebuild.py
index a0272a3..883e345 100644
--- a/lib/lp/charms/subscribers/charmrecipebuild.py
+++ b/lib/lp/charms/subscribers/charmrecipebuild.py
@@ -6,9 +6,7 @@
 from zope.component import getUtility
 
 from lp.buildmaster.enums import BuildStatus
-from lp.charms.interfaces.charmrecipe import (
-    CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG,
-    )
+from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG
 from lp.charms.interfaces.charmrecipebuild import ICharmRecipeBuild
 from lp.charms.interfaces.charmrecipebuildjob import ICharmhubUploadJobSource
 from lp.services.features import getFeatureFlag
@@ -23,12 +21,17 @@ def _trigger_charm_recipe_build_webhook(build, action):
         payload = {
             "recipe_build": canonical_url(build, force_local_path=True),
             "action": action,
-            }
-        payload.update(compose_webhook_payload(
-            ICharmRecipeBuild, build,
-            ["recipe", "build_request", "status", "store_upload_status"]))
+        }
+        payload.update(
+            compose_webhook_payload(
+                ICharmRecipeBuild,
+                build,
+                ["recipe", "build_request", "status", "store_upload_status"],
+            )
+        )
         getUtility(IWebhookSet).trigger(
-            build.recipe, "charm-recipe:build:0.1", payload)
+            build.recipe, "charm-recipe:build:0.1", payload
+        )
 
 
 def charm_recipe_build_created(build, event):
@@ -41,16 +44,20 @@ def charm_recipe_build_modified(build, event):
     if event.edited_fields is not None:
         status_changed = "status" in event.edited_fields
         store_upload_status_changed = (
-            "store_upload_status" in event.edited_fields)
+            "store_upload_status" in event.edited_fields
+        )
         if status_changed or store_upload_status_changed:
             _trigger_charm_recipe_build_webhook(build, "status-changed")
         if status_changed:
             if build.status == BuildStatus.FULLYBUILT:
-                if (build.recipe.can_upload_to_store and
-                        build.recipe.store_upload):
+                if (
+                    build.recipe.can_upload_to_store
+                    and build.recipe.store_upload
+                ):
                     log.info("Scheduling upload of %r to the store." % build)
                     getUtility(ICharmhubUploadJobSource).create(build)
                 else:
                     log.info(
-                        "%r is not configured for upload to the store." %
-                        build.recipe)
+                        "%r is not configured for upload to the store."
+                        % build.recipe
+                    )
diff --git a/lib/lp/charms/tests/test_charmbase.py b/lib/lp/charms/tests/test_charmbase.py
index 4d6d175..c49bdce 100644
--- a/lib/lp/charms/tests/test_charmbase.py
+++ b/lib/lp/charms/tests/test_charmbase.py
@@ -3,34 +3,25 @@
 
 """Test bases for charms."""
 
-from testtools.matchers import (
-    ContainsDict,
-    Equals,
-    )
-from zope.component import (
-    getAdapter,
-    getUtility,
-    )
+from testtools.matchers import ContainsDict, Equals
+from zope.component import getAdapter, getUtility
 
 from lp.app.interfaces.security import IAuthorization
 from lp.charms.interfaces.charmbase import (
     ICharmBase,
     ICharmBaseSet,
     NoSuchCharmBase,
-    )
+)
 from lp.services.webapp.interfaces import OAuthPermission
 from lp.testing import (
+    TestCaseWithFactory,
     admin_logged_in,
     api_url,
     celebrity_logged_in,
     logout,
     person_logged_in,
-    TestCaseWithFactory,
-    )
-from lp.testing.layers import (
-    DatabaseFunctionalLayer,
-    ZopelessDatabaseLayer,
-    )
+)
+from lp.testing.layers import DatabaseFunctionalLayer, ZopelessDatabaseLayer
 from lp.testing.pages import webservice_for_person
 
 
@@ -54,10 +45,12 @@ class TestCharmBase(TestCaseWithFactory):
         charm_base = self.factory.makeCharmBase(distro_series=distro_series)
         charm_base_set = getUtility(ICharmBaseSet)
         self.assertEqual(
-            charm_base, charm_base_set.getByDistroSeries(distro_series))
+            charm_base, charm_base_set.getByDistroSeries(distro_series)
+        )
         charm_base.destroySelf()
         self.assertRaises(
-            NoSuchCharmBase, charm_base_set.getByDistroSeries, distro_series)
+            NoSuchCharmBase, charm_base_set.getByDistroSeries, distro_series
+        )
 
 
 class TestCharmBaseProcessors(TestCaseWithFactory):
@@ -67,32 +60,40 @@ class TestCharmBaseProcessors(TestCaseWithFactory):
     def setUp(self):
         super().setUp(user="foo.bar@xxxxxxxxxxxxx")
         self.unrestricted_procs = [
-            self.factory.makeProcessor() for _ in range(3)]
+            self.factory.makeProcessor() for _ in range(3)
+        ]
         self.restricted_procs = [
             self.factory.makeProcessor(restricted=True, build_by_default=False)
-            for _ in range(2)]
+            for _ in range(2)
+        ]
         self.procs = self.unrestricted_procs + self.restricted_procs
         self.factory.makeProcessor()
         self.distroseries = self.factory.makeDistroSeries()
         for processor in self.procs:
             self.factory.makeDistroArchSeries(
-                distroseries=self.distroseries, architecturetag=processor.name,
-                processor=processor)
+                distroseries=self.distroseries,
+                architecturetag=processor.name,
+                processor=processor,
+            )
 
     def test_new_default_processors(self):
         # CharmBaseSet.new creates a CharmBaseArch for each available
         # Processor for the corresponding series.
         charm_base = getUtility(ICharmBaseSet).new(
             registrant=self.factory.makePerson(),
-            distro_series=self.distroseries, build_snap_channels={})
+            distro_series=self.distroseries,
+            build_snap_channels={},
+        )
         self.assertContentEqual(self.procs, charm_base.processors)
 
     def test_new_override_processors(self):
         # CharmBaseSet.new can be given a custom set of processors.
         charm_base = getUtility(ICharmBaseSet).new(
             registrant=self.factory.makePerson(),
-            distro_series=self.distroseries, build_snap_channels={},
-            processors=self.procs[:2])
+            distro_series=self.distroseries,
+            build_snap_channels={},
+            processors=self.procs[:2],
+        )
         self.assertContentEqual(self.procs[:2], charm_base.processors)
 
     def test_set(self):
@@ -116,15 +117,19 @@ class TestCharmBaseSet(TestCaseWithFactory):
         charm_base = self.factory.makeCharmBase(distro_series=distro_series)
         self.factory.makeCharmBase()
         self.assertEqual(
-            charm_base, charm_base_set.getByDistroSeries(distro_series))
+            charm_base, charm_base_set.getByDistroSeries(distro_series)
+        )
         self.assertRaises(
-            NoSuchCharmBase, charm_base_set.getByDistroSeries,
-            self.factory.makeDistroSeries())
+            NoSuchCharmBase,
+            charm_base_set.getByDistroSeries,
+            self.factory.makeDistroSeries(),
+        )
 
     def test_getAll(self):
         charm_bases = [self.factory.makeCharmBase() for _ in range(3)]
         self.assertContentEqual(
-            charm_bases, getUtility(ICharmBaseSet).getAll())
+            charm_bases, getUtility(ICharmBaseSet).getAll()
+        )
 
 
 class TestCharmBaseWebservice(TestCaseWithFactory):
@@ -137,11 +142,15 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
         distroseries = self.factory.makeDistroSeries()
         distroseries_url = api_url(distroseries)
         webservice = webservice_for_person(
-            person, permission=OAuthPermission.WRITE_PUBLIC)
+            person, permission=OAuthPermission.WRITE_PUBLIC
+        )
         webservice.default_api_version = "devel"
         response = webservice.named_post(
-            "/+charm-bases", "new", distro_series=distroseries_url,
-            build_snap_channels={"charmcraft": "stable"})
+            "/+charm-bases",
+            "new",
+            distro_series=distroseries_url,
+            build_snap_channels={"charmcraft": "stable"},
+        )
         self.assertEqual(401, response.status)
 
     def test_new(self):
@@ -150,22 +159,35 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
         distroseries = self.factory.makeDistroSeries()
         distroseries_url = api_url(distroseries)
         webservice = webservice_for_person(
-            person, permission=OAuthPermission.WRITE_PUBLIC)
+            person, permission=OAuthPermission.WRITE_PUBLIC
+        )
         webservice.default_api_version = "devel"
         logout()
         response = webservice.named_post(
-            "/+charm-bases", "new", distro_series=distroseries_url,
-            build_snap_channels={"charmcraft": "stable"})
+            "/+charm-bases",
+            "new",
+            distro_series=distroseries_url,
+            build_snap_channels={"charmcraft": "stable"},
+        )
         self.assertEqual(201, response.status)
         charm_base = webservice.get(response.getHeader("Location")).jsonBody()
         with person_logged_in(person):
-            self.assertThat(charm_base, ContainsDict({
-                "registrant_link": Equals(
-                    webservice.getAbsoluteUrl(api_url(person))),
-                "distro_series_link": Equals(
-                    webservice.getAbsoluteUrl(distroseries_url)),
-                "build_snap_channels": Equals({"charmcraft": "stable"}),
-                }))
+            self.assertThat(
+                charm_base,
+                ContainsDict(
+                    {
+                        "registrant_link": Equals(
+                            webservice.getAbsoluteUrl(api_url(person))
+                        ),
+                        "distro_series_link": Equals(
+                            webservice.getAbsoluteUrl(distroseries_url)
+                        ),
+                        "build_snap_channels": Equals(
+                            {"charmcraft": "stable"}
+                        ),
+                    }
+                ),
+            )
 
     def test_new_duplicate_distro_series(self):
         # An attempt to create a CharmBase with a duplicate distro series is
@@ -175,21 +197,30 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
         distroseries_str = str(distroseries)
         distroseries_url = api_url(distroseries)
         webservice = webservice_for_person(
-            person, permission=OAuthPermission.WRITE_PUBLIC)
+            person, permission=OAuthPermission.WRITE_PUBLIC
+        )
         webservice.default_api_version = "devel"
         logout()
         response = webservice.named_post(
-            "/+charm-bases", "new", distro_series=distroseries_url,
-            build_snap_channels={"charmcraft": "stable"})
+            "/+charm-bases",
+            "new",
+            distro_series=distroseries_url,
+            build_snap_channels={"charmcraft": "stable"},
+        )
         self.assertEqual(201, response.status)
         response = webservice.named_post(
-            "/+charm-bases", "new", distro_series=distroseries_url,
-            build_snap_channels={"charmcraft": "stable"})
+            "/+charm-bases",
+            "new",
+            distro_series=distroseries_url,
+            build_snap_channels={"charmcraft": "stable"},
+        )
         self.assertEqual(400, response.status)
         self.assertEqual(
-            ("%s is already in use by another base." %
-             distroseries_str).encode(),
-            response.body)
+            (
+                "%s is already in use by another base." % distroseries_str
+            ).encode(),
+            response.body,
+        )
 
     def test_getByDistroSeries(self):
         # lp.charm_bases.getByDistroSeries returns a matching CharmBase.
@@ -197,17 +228,21 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
         distroseries = self.factory.makeDistroSeries()
         distroseries_url = api_url(distroseries)
         webservice = webservice_for_person(
-            person, permission=OAuthPermission.READ_PUBLIC)
+            person, permission=OAuthPermission.READ_PUBLIC
+        )
         webservice.default_api_version = "devel"
         with celebrity_logged_in("registry_experts"):
             self.factory.makeCharmBase(distro_series=distroseries)
         response = webservice.named_get(
-            "/+charm-bases", "getByDistroSeries",
-            distro_series=distroseries_url)
+            "/+charm-bases",
+            "getByDistroSeries",
+            distro_series=distroseries_url,
+        )
         self.assertEqual(200, response.status)
         self.assertEqual(
             webservice.getAbsoluteUrl(distroseries_url),
-            response.jsonBody()["distro_series_link"])
+            response.jsonBody()["distro_series_link"],
+        )
 
     def test_getByDistroSeries_missing(self):
         # lp.charm_bases.getByDistroSeries returns 404 for a non-existent
@@ -217,47 +252,64 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
         distroseries_str = str(distroseries)
         distroseries_url = api_url(distroseries)
         webservice = webservice_for_person(
-            person, permission=OAuthPermission.READ_PUBLIC)
+            person, permission=OAuthPermission.READ_PUBLIC
+        )
         webservice.default_api_version = "devel"
         logout()
         response = webservice.named_get(
-            "/+charm-bases", "getByDistroSeries",
-            distro_series=distroseries_url)
+            "/+charm-bases",
+            "getByDistroSeries",
+            distro_series=distroseries_url,
+        )
         self.assertEqual(404, response.status)
         self.assertEqual(
-            ("No base for %s." % distroseries_str).encode(), response.body)
+            ("No base for %s." % distroseries_str).encode(), response.body
+        )
 
     def setUpProcessors(self):
         self.unrestricted_procs = [
-            self.factory.makeProcessor() for _ in range(3)]
+            self.factory.makeProcessor() for _ in range(3)
+        ]
         self.unrestricted_proc_names = [
-            processor.name for processor in self.unrestricted_procs]
+            processor.name for processor in self.unrestricted_procs
+        ]
         self.restricted_procs = [
             self.factory.makeProcessor(restricted=True, build_by_default=False)
-            for _ in range(2)]
+            for _ in range(2)
+        ]
         self.restricted_proc_names = [
-            processor.name for processor in self.restricted_procs]
+            processor.name for processor in self.restricted_procs
+        ]
         self.procs = self.unrestricted_procs + self.restricted_procs
         self.factory.makeProcessor()
         self.distroseries = self.factory.makeDistroSeries()
         for processor in self.procs:
             self.factory.makeDistroArchSeries(
-                distroseries=self.distroseries, architecturetag=processor.name,
-                processor=processor)
+                distroseries=self.distroseries,
+                architecturetag=processor.name,
+                processor=processor,
+            )
 
     def setProcessors(self, user, charm_base_url, names):
         ws = webservice_for_person(
-            user, permission=OAuthPermission.WRITE_PUBLIC)
+            user, permission=OAuthPermission.WRITE_PUBLIC
+        )
         return ws.named_post(
-            charm_base_url, "setProcessors",
+            charm_base_url,
+            "setProcessors",
             processors=["/+processors/%s" % name for name in names],
-            api_version="devel")
+            api_version="devel",
+        )
 
     def assertProcessors(self, user, charm_base_url, names):
-        body = webservice_for_person(user).get(
-            charm_base_url + "/processors", api_version="devel").jsonBody()
+        body = (
+            webservice_for_person(user)
+            .get(charm_base_url + "/processors", api_version="devel")
+            .jsonBody()
+        )
         self.assertContentEqual(
-            names, [entry["name"] for entry in body["entries"]])
+            names, [entry["name"] for entry in body["entries"]]
+        )
 
     def test_setProcessors_admin(self):
         """An admin can change the supported processor set."""
@@ -265,49 +317,62 @@ class TestCharmBaseWebservice(TestCaseWithFactory):
         with admin_logged_in():
             charm_base = self.factory.makeCharmBase(
                 distro_series=self.distroseries,
-                processors=self.unrestricted_procs)
+                processors=self.unrestricted_procs,
+            )
             charm_base_url = api_url(charm_base)
         admin = self.factory.makeAdministrator()
         self.assertProcessors(
-            admin, charm_base_url, self.unrestricted_proc_names)
+            admin, charm_base_url, self.unrestricted_proc_names
+        )
 
         response = self.setProcessors(
-            admin, charm_base_url,
-            [self.unrestricted_proc_names[0], self.restricted_proc_names[0]])
+            admin,
+            charm_base_url,
+            [self.unrestricted_proc_names[0], self.restricted_proc_names[0]],
+        )
         self.assertEqual(200, response.status)
         self.assertProcessors(
-            admin, charm_base_url,
-            [self.unrestricted_proc_names[0], self.restricted_proc_names[0]])
+            admin,
+            charm_base_url,
+            [self.unrestricted_proc_names[0], self.restricted_proc_names[0]],
+        )
 
     def test_setProcessors_non_admin_forbidden(self):
         """Only admins and registry experts can call setProcessors."""
         self.setUpProcessors()
         with admin_logged_in():
             charm_base = self.factory.makeCharmBase(
-                distro_series=self.distroseries)
+                distro_series=self.distroseries
+            )
             charm_base_url = api_url(charm_base)
         person = self.factory.makePerson()
 
         response = self.setProcessors(
-            person, charm_base_url, [self.unrestricted_proc_names[0]])
+            person, charm_base_url, [self.unrestricted_proc_names[0]]
+        )
         self.assertEqual(401, response.status)
 
     def test_collection(self):
         # lp.charm_bases is a collection of all CharmBases.
         person = self.factory.makePerson()
         webservice = webservice_for_person(
-            person, permission=OAuthPermission.READ_PUBLIC)
+            person, permission=OAuthPermission.READ_PUBLIC
+        )
         webservice.default_api_version = "devel"
         distroseries_urls = []
         with celebrity_logged_in("registry_experts"):
             for i in range(3):
                 distroseries = self.factory.makeDistroSeries()
                 distroseries_urls.append(
-                    webservice.getAbsoluteUrl(api_url(distroseries)))
+                    webservice.getAbsoluteUrl(api_url(distroseries))
+                )
                 self.factory.makeCharmBase(distro_series=distroseries)
         response = webservice.get("/+charm-bases")
         self.assertEqual(200, response.status)
         self.assertContentEqual(
             distroseries_urls,
-            [entry["distro_series_link"]
-             for entry in response.jsonBody()["entries"]])
+            [
+                entry["distro_series_link"]
+                for entry in response.jsonBody()["entries"]
+            ],
+        )
diff --git a/lib/lp/charms/tests/test_charmhubclient.py b/lib/lp/charms/tests/test_charmhubclient.py
index 018709d..771941f 100644
--- a/lib/lp/charms/tests/test_charmhubclient.py
+++ b/lib/lp/charms/tests/test_charmhubclient.py
@@ -9,15 +9,13 @@ import io
 import json
 from urllib.parse import quote
 
-from lazr.restful.utils import get_current_browser_request
 import multipart
+import responses
+import transaction
+from lazr.restful.utils import get_current_browser_request
 from nacl.public import PrivateKey
-from pymacaroons import (
-    Macaroon,
-    Verifier,
-    )
+from pymacaroons import Macaroon, Verifier
 from pymacaroons.serializers import JsonSerializer
-import responses
 from testtools.matchers import (
     AfterPreprocessing,
     ContainsDict,
@@ -29,8 +27,7 @@ from testtools.matchers import (
     MatchesListwise,
     MatchesStructure,
     Mismatch,
-    )
-import transaction
+)
 from zope.component import getUtility
 from zope.security.proxy import removeSecurityProxy
 
@@ -45,7 +42,7 @@ from lp.charms.interfaces.charmhubclient import (
     UnauthorizedUploadResponse,
     UploadFailedResponse,
     UploadNotReviewedYetResponse,
-    )
+)
 from lp.charms.interfaces.charmrecipe import CHARM_RECIPE_ALLOW_CREATE
 from lp.services.crypto.interfaces import IEncryptedContainer
 from lp.services.features.testing import FeatureFixture
@@ -72,40 +69,76 @@ class MacaroonVerifies(Matcher):
 class RequestMatches(MatchesAll):
     """Matches a request with the specified attributes."""
 
-    def __init__(self, macaroons=None, auth=None, json_data=None,
-                 file_data=None, **kwargs):
+    def __init__(
+        self,
+        macaroons=None,
+        auth=None,
+        json_data=None,
+        file_data=None,
+        **kwargs
+    ):
         matchers = []
         kwargs = dict(kwargs)
         if macaroons is not None:
-            matchers.append(MatchesStructure(headers=ContainsDict({
-                "Macaroons": AfterPreprocessing(
-                    lambda v: json.loads(
-                        base64.b64decode(v.encode()).decode()),
-                    Equals([json.loads(m) for m in macaroons])),
-                })))
+            matchers.append(
+                MatchesStructure(
+                    headers=ContainsDict(
+                        {
+                            "Macaroons": AfterPreprocessing(
+                                lambda v: json.loads(
+                                    base64.b64decode(v.encode()).decode()
+                                ),
+                                Equals([json.loads(m) for m in macaroons]),
+                            ),
+                        }
+                    )
+                )
+            )
         if auth is not None:
             auth_scheme, auth_params_matcher = auth
-            matchers.append(MatchesStructure(headers=ContainsDict({
-                "Authorization": AfterPreprocessing(
-                    lambda v: v.split(" ", 1),
-                    MatchesListwise([
-                        Equals(auth_scheme),
-                        auth_params_matcher,
-                        ])),
-                })))
+            matchers.append(
+                MatchesStructure(
+                    headers=ContainsDict(
+                        {
+                            "Authorization": AfterPreprocessing(
+                                lambda v: v.split(" ", 1),
+                                MatchesListwise(
+                                    [
+                                        Equals(auth_scheme),
+                                        auth_params_matcher,
+                                    ]
+                                ),
+                            ),
+                        }
+                    )
+                )
+            )
         if json_data is not None:
-            matchers.append(MatchesStructure(body=AfterPreprocessing(
-                lambda b: json.loads(b.decode()), Equals(json_data))))
+            matchers.append(
+                MatchesStructure(
+                    body=AfterPreprocessing(
+                        lambda b: json.loads(b.decode()), Equals(json_data)
+                    )
+                )
+            )
         elif file_data is not None:
-            matchers.append(AfterPreprocessing(
-                lambda r: multipart.parse_form_data({
-                    "REQUEST_METHOD": r.method,
-                    "CONTENT_TYPE": r.headers["Content-Type"],
-                    "CONTENT_LENGTH": r.headers["Content-Length"],
-                    "wsgi.input": io.BytesIO(
-                        r.body.read() if hasattr(r.body, "read") else r.body),
-                    })[1],
-                MatchesDict(file_data)))
+            matchers.append(
+                AfterPreprocessing(
+                    lambda r: multipart.parse_form_data(
+                        {
+                            "REQUEST_METHOD": r.method,
+                            "CONTENT_TYPE": r.headers["Content-Type"],
+                            "CONTENT_LENGTH": r.headers["Content-Length"],
+                            "wsgi.input": io.BytesIO(
+                                r.body.read()
+                                if hasattr(r.body, "read")
+                                else r.body
+                            ),
+                        }
+                    )[1],
+                    MatchesDict(file_data),
+                )
+            )
         if kwargs:
             matchers.append(MatchesStructure(**kwargs))
         super().__init__(*matchers)
@@ -121,7 +154,8 @@ class TestCharmhubClient(TestCaseWithFactory):
         self.pushConfig(
             "charms",
             charmhub_url="http://charmhub.example/";,
-            charmhub_storage_url="http://storage.charmhub.example/";)
+            charmhub_storage_url="http://storage.charmhub.example/";,
+        )
         self.client = getUtility(ICharmhubClient)
 
     def _setUpSecretStorage(self):
@@ -129,86 +163,121 @@ class TestCharmhubClient(TestCaseWithFactory):
         self.pushConfig(
             "charms",
             charmhub_secrets_public_key=base64.b64encode(
-                bytes(self.private_key.public_key)).decode(),
+                bytes(self.private_key.public_key)
+            ).decode(),
             charmhub_secrets_private_key=base64.b64encode(
-                bytes(self.private_key)).decode())
+                bytes(self.private_key)
+            ).decode(),
+        )
 
     def _makeStoreSecrets(self):
         self.exchanged_key = hashlib.sha256(
-            self.factory.getUniqueBytes()).hexdigest()
+            self.factory.getUniqueBytes()
+        ).hexdigest()
         exchanged_macaroon = Macaroon(key=self.exchanged_key)
         container = getUtility(IEncryptedContainer, "charmhub-secrets")
         return {
-            "exchanged_encrypted": removeSecurityProxy(container.encrypt(
-                exchanged_macaroon.serialize().encode())),
-            }
+            "exchanged_encrypted": removeSecurityProxy(
+                container.encrypt(exchanged_macaroon.serialize().encode())
+            ),
+        }
 
     def _addUnscannedUploadResponse(self):
         responses.add(
-            "POST", "http://storage.charmhub.example/unscanned-upload/";,
-            json={"successful": True, "upload_id": 1})
+            "POST",
+            "http://storage.charmhub.example/unscanned-upload/";,
+            json={"successful": True, "upload_id": 1},
+        )
 
     def _addCharmPushResponse(self, name):
         responses.add(
             "POST",
             "http://charmhub.example/v1/charm/{}/revisions".format(
-                quote(name)),
+                quote(name)
+            ),
             status=200,
             json={
                 "status-url": (
                     "/v1/charm/{}/revisions/review?upload-id=123".format(
-                        quote(name))),
-                })
+                        quote(name)
+                    )
+                ),
+            },
+        )
 
     def _addCharmReleaseResponse(self, name):
         responses.add(
             "POST",
             "http://charmhub.example/v1/charm/{}/releases".format(quote(name)),
-            json={})
+            json={},
+        )
 
     @responses.activate
     def test_requestPackageUploadPermission(self):
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens";,
-            json={"macaroon": "sentinel"})
+            "POST",
+            "http://charmhub.example/v1/tokens";,
+            json={"macaroon": "sentinel"},
+        )
         macaroon = self.client.requestPackageUploadPermission("test-charm")
-        self.assertThat(responses.calls[-1].request, RequestMatches(
-            url=Equals("http://charmhub.example/v1/tokens";),
-            method=Equals("POST"),
-            json_data={
-                "description": "test-charm for launchpad.test",
-                "packages": [{"type": "charm", "name": "test-charm"}],
-                "permissions": [
-                    "package-manage-releases",
-                    "package-manage-revisions",
-                    "package-view-revisions",
+        self.assertThat(
+            responses.calls[-1].request,
+            RequestMatches(
+                url=Equals("http://charmhub.example/v1/tokens";),
+                method=Equals("POST"),
+                json_data={
+                    "description": "test-charm for launchpad.test",
+                    "packages": [{"type": "charm", "name": "test-charm"}],
+                    "permissions": [
+                        "package-manage-releases",
+                        "package-manage-revisions",
+                        "package-view-revisions",
                     ],
-                }))
+                },
+            ),
+        )
         self.assertEqual("sentinel", macaroon)
         request = get_current_browser_request()
         start, stop = get_request_timeline(request).actions[-2:]
-        self.assertThat(start, MatchesStructure.byEquality(
-            category="request-charm-upload-macaroon-start",
-            detail="test-charm"))
-        self.assertThat(stop, MatchesStructure.byEquality(
-            category="request-charm-upload-macaroon-stop",
-            detail="test-charm"))
+        self.assertThat(
+            start,
+            MatchesStructure.byEquality(
+                category="request-charm-upload-macaroon-start",
+                detail="test-charm",
+            ),
+        )
+        self.assertThat(
+            stop,
+            MatchesStructure.byEquality(
+                category="request-charm-upload-macaroon-stop",
+                detail="test-charm",
+            ),
+        )
 
     @responses.activate
     def test_requestPackageUploadPermission_missing_macaroon(self):
         responses.add("POST", "http://charmhub.example/v1/tokens";, json={})
         self.assertRaisesWithContent(
-            BadRequestPackageUploadResponse, "{}",
-            self.client.requestPackageUploadPermission, "test-charm")
+            BadRequestPackageUploadResponse,
+            "{}",
+            self.client.requestPackageUploadPermission,
+            "test-charm",
+        )
 
     @responses.activate
     def test_requestPackageUploadPermission_error(self):
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens";,
-            status=503, json={"error-list": [{"message": "Failed"}]})
+            "POST",
+            "http://charmhub.example/v1/tokens";,
+            status=503,
+            json={"error-list": [{"message": "Failed"}]},
+        )
         self.assertRaisesWithContent(
-            BadRequestPackageUploadResponse, "Failed",
-            self.client.requestPackageUploadPermission, "test-charm")
+            BadRequestPackageUploadResponse,
+            "Failed",
+            self.client.requestPackageUploadPermission,
+            "test-charm",
+        )
 
     @responses.activate
     def test_requestPackageUploadPermission_404(self):
@@ -216,86 +285,124 @@ class TestCharmhubClient(TestCaseWithFactory):
         self.assertRaisesWithContent(
             BadRequestPackageUploadResponse,
             "404 Client Error: Not Found",
-            self.client.requestPackageUploadPermission, "test-charm")
+            self.client.requestPackageUploadPermission,
+            "test-charm",
+        )
 
     @responses.activate
     def test_exchangeMacaroons(self):
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens/exchange";,
-            json={"macaroon": "sentinel"})
+            "POST",
+            "http://charmhub.example/v1/tokens/exchange";,
+            json={"macaroon": "sentinel"},
+        )
         root_macaroon = Macaroon(version=2)
         root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
         unbound_discharge_macaroon = Macaroon(version=2)
         unbound_discharge_macaroon_raw = unbound_discharge_macaroon.serialize(
-            JsonSerializer())
+            JsonSerializer()
+        )
         discharge_macaroon_raw = root_macaroon.prepare_for_request(
-            unbound_discharge_macaroon).serialize(JsonSerializer())
+            unbound_discharge_macaroon
+        ).serialize(JsonSerializer())
         exchanged_macaroon_raw = self.client.exchangeMacaroons(
-            root_macaroon_raw, unbound_discharge_macaroon_raw)
-        self.assertThat(responses.calls[-1].request, RequestMatches(
-            url=Equals("http://charmhub.example/v1/tokens/exchange";),
-            method=Equals("POST"),
-            macaroons=[root_macaroon_raw, discharge_macaroon_raw],
-            json_data={}))
+            root_macaroon_raw, unbound_discharge_macaroon_raw
+        )
+        self.assertThat(
+            responses.calls[-1].request,
+            RequestMatches(
+                url=Equals("http://charmhub.example/v1/tokens/exchange";),
+                method=Equals("POST"),
+                macaroons=[root_macaroon_raw, discharge_macaroon_raw],
+                json_data={},
+            ),
+        )
         self.assertEqual("sentinel", exchanged_macaroon_raw)
         request = get_current_browser_request()
         start, stop = get_request_timeline(request).actions[-2:]
-        self.assertThat(start, MatchesStructure.byEquality(
-            category="exchange-macaroons-start", detail=""))
-        self.assertThat(stop, MatchesStructure.byEquality(
-            category="exchange-macaroons-stop", detail=""))
+        self.assertThat(
+            start,
+            MatchesStructure.byEquality(
+                category="exchange-macaroons-start", detail=""
+            ),
+        )
+        self.assertThat(
+            stop,
+            MatchesStructure.byEquality(
+                category="exchange-macaroons-stop", detail=""
+            ),
+        )
 
     @responses.activate
     def test_exchangeMacaroons_missing_macaroon(self):
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens/exchange";, json={})
+            "POST", "http://charmhub.example/v1/tokens/exchange";, json={}
+        )
         root_macaroon_raw = Macaroon(version=2).serialize(JsonSerializer())
         discharge_macaroon_raw = Macaroon(version=2).serialize(
-            JsonSerializer())
+            JsonSerializer()
+        )
         self.assertRaisesWithContent(
-            BadExchangeMacaroonsResponse, "{}",
+            BadExchangeMacaroonsResponse,
+            "{}",
             self.client.exchangeMacaroons,
-            root_macaroon_raw, discharge_macaroon_raw)
+            root_macaroon_raw,
+            discharge_macaroon_raw,
+        )
 
     @responses.activate
     def test_exchangeMacaroons_error(self):
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens/exchange";,
+            "POST",
+            "http://charmhub.example/v1/tokens/exchange";,
             status=401,
-            json={"error-list": [{"message": "Exchange window expired"}]})
+            json={"error-list": [{"message": "Exchange window expired"}]},
+        )
         root_macaroon_raw = Macaroon(version=2).serialize(JsonSerializer())
         discharge_macaroon_raw = Macaroon(version=2).serialize(
-            JsonSerializer())
+            JsonSerializer()
+        )
         self.assertRaisesWithContent(
-            BadExchangeMacaroonsResponse, "Exchange window expired",
+            BadExchangeMacaroonsResponse,
+            "Exchange window expired",
             self.client.exchangeMacaroons,
-            root_macaroon_raw, discharge_macaroon_raw)
+            root_macaroon_raw,
+            discharge_macaroon_raw,
+        )
 
     @responses.activate
     def test_exchangeMacaroons_404(self):
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens/exchange";, status=404)
+            "POST", "http://charmhub.example/v1/tokens/exchange";, status=404
+        )
         root_macaroon_raw = Macaroon(version=2).serialize(JsonSerializer())
         discharge_macaroon_raw = Macaroon(version=2).serialize(
-            JsonSerializer())
+            JsonSerializer()
+        )
         self.assertRaisesWithContent(
             BadExchangeMacaroonsResponse,
             "404 Client Error: Not Found",
             self.client.exchangeMacaroons,
-            root_macaroon_raw, discharge_macaroon_raw)
+            root_macaroon_raw,
+            discharge_macaroon_raw,
+        )
 
     def makeUploadableCharmRecipeBuild(self, store_secrets=None):
         if store_secrets is None:
             store_secrets = self._makeStoreSecrets()
         recipe = self.factory.makeCharmRecipe(
             store_upload=True,
-            store_name="test-charm", store_secrets=store_secrets)
+            store_name="test-charm",
+            store_secrets=store_secrets,
+        )
         build = self.factory.makeCharmRecipeBuild(recipe=recipe)
         charm_lfa = self.factory.makeLibraryFileAlias(
-            filename="test-charm.charm", content="dummy charm content")
+            filename="test-charm.charm", content="dummy charm content"
+        )
         self.factory.makeCharmFile(build=build, library_file=charm_lfa)
         manifest_lfa = self.factory.makeLibraryFileAlias(
-            filename="test-charm.manifest", content="dummy manifest content")
+            filename="test-charm.manifest", content="dummy manifest content"
+        )
         self.factory.makeCharmFile(build=build, library_file=manifest_lfa)
         build.updateStatus(BuildStatus.BUILDING)
         build.updateStatus(BuildStatus.FULLYBUILT)
@@ -304,7 +411,8 @@ class TestCharmhubClient(TestCaseWithFactory):
     @responses.activate
     def test_uploadFile(self):
         charm_lfa = self.factory.makeLibraryFileAlias(
-            filename="test-charm.charm", content="dummy charm content")
+            filename="test-charm.charm", content="dummy charm content"
+        )
         transaction.commit()
         self._addUnscannedUploadResponse()
         # XXX cjwatson 2021-08-19: Use
@@ -312,36 +420,45 @@ class TestCharmhubClient(TestCaseWithFactory):
         with dbuser("charm-build-job"):
             self.assertEqual(1, self.client.uploadFile(charm_lfa))
         requests = [call.request for call in responses.calls]
-        self.assertThat(requests, MatchesListwise([
-            RequestMatches(
-                url=Equals(
-                    "http://storage.charmhub.example/unscanned-upload/";),
-                method=Equals("POST"),
-                file_data={
-                    "binary": MatchesStructure.byEquality(
-                        name="binary", filename="test-charm.charm",
-                        value="dummy charm content",
-                        content_type="application/octet-stream",
-                        )}),
-            ]))
+        request_matcher = RequestMatches(
+            url=Equals("http://storage.charmhub.example/unscanned-upload/";),
+            method=Equals("POST"),
+            file_data={
+                "binary": MatchesStructure.byEquality(
+                    name="binary",
+                    filename="test-charm.charm",
+                    value="dummy charm content",
+                    content_type="application/octet-stream",
+                )
+            },
+        )
+        self.assertThat(requests, MatchesListwise([request_matcher]))
 
     @responses.activate
     def test_uploadFile_error(self):
         charm_lfa = self.factory.makeLibraryFileAlias(
-            filename="test-charm.charm", content="dummy charm content")
+            filename="test-charm.charm", content="dummy charm content"
+        )
         transaction.commit()
         responses.add(
-            "POST", "http://storage.charmhub.example/unscanned-upload/";,
-            status=502, body="The proxy exploded.\n")
+            "POST",
+            "http://storage.charmhub.example/unscanned-upload/";,
+            status=502,
+            body="The proxy exploded.\n",
+        )
         # XXX cjwatson 2021-08-19: Use
         # config.ICharmhubUploadJobSource.dbuser once that job exists.
         with dbuser("charm-build-job"):
             err = self.assertRaises(
-                UploadFailedResponse, self.client.uploadFile, charm_lfa)
+                UploadFailedResponse, self.client.uploadFile, charm_lfa
+            )
             self.assertEqual("502 Server Error: Bad Gateway", str(err))
-            self.assertThat(err, MatchesStructure(
-                detail=Equals("The proxy exploded.\n"),
-                can_retry=Is(True)))
+            self.assertThat(
+                err,
+                MatchesStructure(
+                    detail=Equals("The proxy exploded.\n"), can_retry=Is(True)
+                ),
+            )
 
     @responses.activate
     def test_push(self):
@@ -354,18 +471,22 @@ class TestCharmhubClient(TestCaseWithFactory):
         with dbuser("charm-build-job"):
             self.assertEqual(
                 "/v1/charm/test-charm/revisions/review?upload-id=123",
-                self.client.push(build, 1))
+                self.client.push(build, 1),
+            )
         requests = [call.request for call in responses.calls]
-        self.assertThat(requests, MatchesListwise([
-            RequestMatches(
-                url=Equals(
-                    "http://charmhub.example/v1/charm/test-charm/revisions";),
-                method=Equals("POST"),
-                headers=ContainsDict(
-                    {"Content-Type": Equals("application/json")}),
-                auth=("Macaroon", MacaroonVerifies(self.exchanged_key)),
-                json_data={"upload-id": 1}),
-            ]))
+        request_matcher = RequestMatches(
+            url=Equals(
+                "http://charmhub.example/v1/charm/test-charm/revisions";
+            ),
+            method=Equals("POST"),
+            headers=ContainsDict({"Content-Type": Equals("application/json")}),
+            auth=(
+                "Macaroon",
+                MacaroonVerifies(self.exchanged_key),
+            ),
+            json_data={"upload-id": 1},
+        )
+        self.assertThat(requests, MatchesListwise([request_matcher]))
 
     @responses.activate
     def test_push_unauthorized(self):
@@ -375,18 +496,23 @@ class TestCharmhubClient(TestCaseWithFactory):
         charm_push_error = {
             "code": "permission-required",
             "message": "Missing required permission: package-manage-revisions",
-            }
+        }
         responses.add(
-            "POST", "http://charmhub.example/v1/charm/test-charm/revisions";,
+            "POST",
+            "http://charmhub.example/v1/charm/test-charm/revisions";,
             status=401,
-            json={"error-list": [charm_push_error]})
+            json={"error-list": [charm_push_error]},
+        )
         # XXX cjwatson 2021-08-19: Use
         # config.ICharmhubUploadJobSource.dbuser once that job exists.
         with dbuser("charm-build-job"):
             self.assertRaisesWithContent(
                 UnauthorizedUploadResponse,
                 "Missing required permission: package-manage-revisions",
-                self.client.push, build, 1)
+                self.client.push,
+                build,
+                1,
+            )
 
     @responses.activate
     def test_checkStatus_pending(self):
@@ -394,7 +520,8 @@ class TestCharmhubClient(TestCaseWithFactory):
         build = self.makeUploadableCharmRecipeBuild()
         status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
         responses.add(
-            "GET", "http://charmhub.example"; + status_url,
+            "GET",
+            "http://charmhub.example"; + status_url,
             json={
                 "revisions": [
                     {
@@ -402,12 +529,16 @@ class TestCharmhubClient(TestCaseWithFactory):
                         "status": "new",
                         "revision": None,
                         "errors": None,
-                        },
-                    ],
-                })
+                    },
+                ],
+            },
+        )
         self.assertRaises(
             UploadNotReviewedYetResponse,
-            self.client.checkStatus, build, status_url)
+            self.client.checkStatus,
+            build,
+            status_url,
+        )
 
     @responses.activate
     def test_checkStatus_error(self):
@@ -415,7 +546,8 @@ class TestCharmhubClient(TestCaseWithFactory):
         build = self.makeUploadableCharmRecipeBuild()
         status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
         responses.add(
-            "GET", "http://charmhub.example"; + status_url,
+            "GET",
+            "http://charmhub.example"; + status_url,
             json={
                 "revisions": [
                     {
@@ -424,13 +556,18 @@ class TestCharmhubClient(TestCaseWithFactory):
                         "revision": None,
                         "errors": [
                             {"code": None, "message": "This charm is broken."},
-                            ],
-                        },
-                    ],
-                })
+                        ],
+                    },
+                ],
+            },
+        )
         self.assertRaisesWithContent(
-            ReviewFailedResponse, "This charm is broken.",
-            self.client.checkStatus, build, status_url)
+            ReviewFailedResponse,
+            "This charm is broken.",
+            self.client.checkStatus,
+            build,
+            status_url,
+        )
 
     @responses.activate
     def test_checkStatus_approved_no_revision(self):
@@ -438,7 +575,8 @@ class TestCharmhubClient(TestCaseWithFactory):
         build = self.makeUploadableCharmRecipeBuild()
         status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
         responses.add(
-            "GET", "http://charmhub.example"; + status_url,
+            "GET",
+            "http://charmhub.example"; + status_url,
             json={
                 "revisions": [
                     {
@@ -446,13 +584,17 @@ class TestCharmhubClient(TestCaseWithFactory):
                         "status": "approved",
                         "revision": None,
                         "errors": None,
-                        },
-                    ],
-                })
+                    },
+                ],
+            },
+        )
         self.assertRaisesWithContent(
             ReviewFailedResponse,
             "Review passed but did not assign a revision.",
-            self.client.checkStatus, build, status_url)
+            self.client.checkStatus,
+            build,
+            status_url,
+        )
 
     @responses.activate
     def test_checkStatus_approved(self):
@@ -460,7 +602,8 @@ class TestCharmhubClient(TestCaseWithFactory):
         build = self.makeUploadableCharmRecipeBuild()
         status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
         responses.add(
-            "GET", "http://charmhub.example"; + status_url,
+            "GET",
+            "http://charmhub.example"; + status_url,
             json={
                 "revisions": [
                     {
@@ -468,17 +611,21 @@ class TestCharmhubClient(TestCaseWithFactory):
                         "status": "approved",
                         "revision": 1,
                         "errors": None,
-                        },
-                    ],
-                })
+                    },
+                ],
+            },
+        )
         self.assertEqual(1, self.client.checkStatus(build, status_url))
         requests = [call.request for call in responses.calls]
-        self.assertThat(requests, MatchesListwise([
-            RequestMatches(
-                url=Equals("http://charmhub.example"; + status_url),
-                method=Equals("GET"),
-                auth=("Macaroon", MacaroonVerifies(self.exchanged_key))),
-            ]))
+        request_matcher = RequestMatches(
+            url=Equals("http://charmhub.example"; + status_url),
+            method=Equals("GET"),
+            auth=(
+                "Macaroon",
+                MacaroonVerifies(self.exchanged_key),
+            ),
+        )
+        self.assertThat(requests, MatchesListwise([request_matcher]))
 
     @responses.activate
     def test_checkStatus_404(self):
@@ -486,58 +633,89 @@ class TestCharmhubClient(TestCaseWithFactory):
         build = self.makeUploadableCharmRecipeBuild()
         status_url = "/v1/charm/test-charm/revisions/review?upload-id=123"
         responses.add(
-            "GET", "http://charmhub.example"; + status_url, status=404)
+            "GET", "http://charmhub.example"; + status_url, status=404
+        )
         self.assertRaisesWithContent(
-            BadReviewStatusResponse, "404 Client Error: Not Found",
-            self.client.checkStatus, build, status_url)
+            BadReviewStatusResponse,
+            "404 Client Error: Not Found",
+            self.client.checkStatus,
+            build,
+            status_url,
+        )
 
     @responses.activate
     def test_release(self):
         self._setUpSecretStorage()
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name="test-charm",
+            store_upload=True,
+            store_name="test-charm",
             store_secrets=self._makeStoreSecrets(),
-            store_channels=["stable", "edge"])
+            store_channels=["stable", "edge"],
+        )
         build = self.factory.makeCharmRecipeBuild(recipe=recipe)
         self._addCharmReleaseResponse("test-charm")
         self.client.release(build, 1)
-        self.assertThat(responses.calls[-1].request, RequestMatches(
-            url=Equals("http://charmhub.example/v1/charm/test-charm/releases";),
-            method=Equals("POST"),
-            headers=ContainsDict({"Content-Type": Equals("application/json")}),
-            auth=("Macaroon", MacaroonVerifies(self.exchanged_key)),
-            json_data=[
-                {"channel": "stable", "revision": 1},
-                {"channel": "edge", "revision": 1},
-                ]))
+        self.assertThat(
+            responses.calls[-1].request,
+            RequestMatches(
+                url=Equals(
+                    "http://charmhub.example/v1/charm/test-charm/releases";
+                ),
+                method=Equals("POST"),
+                headers=ContainsDict(
+                    {"Content-Type": Equals("application/json")}
+                ),
+                auth=("Macaroon", MacaroonVerifies(self.exchanged_key)),
+                json_data=[
+                    {"channel": "stable", "revision": 1},
+                    {"channel": "edge", "revision": 1},
+                ],
+            ),
+        )
 
     @responses.activate
     def test_release_error(self):
         self._setUpSecretStorage()
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name="test-charm",
+            store_upload=True,
+            store_name="test-charm",
             store_secrets=self._makeStoreSecrets(),
-            store_channels=["stable", "edge"])
+            store_channels=["stable", "edge"],
+        )
         build = self.factory.makeCharmRecipeBuild(recipe=recipe)
         responses.add(
-            "POST", "http://charmhub.example/v1/charm/test-charm/releases";,
+            "POST",
+            "http://charmhub.example/v1/charm/test-charm/releases";,
             status=503,
-            json={"error-list": [{"message": "Failed to publish"}]})
+            json={"error-list": [{"message": "Failed to publish"}]},
+        )
         self.assertRaisesWithContent(
-            ReleaseFailedResponse, "Failed to publish",
-            self.client.release, build, 1)
+            ReleaseFailedResponse,
+            "Failed to publish",
+            self.client.release,
+            build,
+            1,
+        )
 
     @responses.activate
     def test_release_404(self):
         self._setUpSecretStorage()
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name="test-charm",
+            store_upload=True,
+            store_name="test-charm",
             store_secrets=self._makeStoreSecrets(),
-            store_channels=["stable", "edge"])
+            store_channels=["stable", "edge"],
+        )
         build = self.factory.makeCharmRecipeBuild(recipe=recipe)
         responses.add(
-            "POST", "http://charmhub.example/v1/charm/test-charm/releases";,
-            status=404)
+            "POST",
+            "http://charmhub.example/v1/charm/test-charm/releases";,
+            status=404,
+        )
         self.assertRaisesWithContent(
-            ReleaseFailedResponse, "404 Client Error: Not Found",
-            self.client.release, build, 1)
+            ReleaseFailedResponse,
+            "404 Client Error: Not Found",
+            self.client.release,
+            build,
+            1,
+        )
diff --git a/lib/lp/charms/tests/test_charmrecipe.py b/lib/lp/charms/tests/test_charmrecipe.py
index b67d542..f7818e8 100644
--- a/lib/lp/charms/tests/test_charmrecipe.py
+++ b/lib/lp/charms/tests/test_charmrecipe.py
@@ -4,20 +4,18 @@
 """Test charm recipes."""
 
 import base64
-from datetime import (
-    datetime,
-    timedelta,
-    )
 import json
+from datetime import datetime, timedelta
 from textwrap import dedent
 
-from fixtures import FakeLogger
 import iso8601
+import pytz
+import responses
+import transaction
+from fixtures import FakeLogger
 from nacl.public import PrivateKey
 from pymacaroons import Macaroon
 from pymacaroons.serializers import JsonSerializer
-import pytz
-import responses
 from storm.exceptions import LostObjectError
 from storm.locals import Store
 from testtools.matchers import (
@@ -32,8 +30,7 @@ from testtools.matchers import (
     MatchesListwise,
     MatchesSetwise,
     MatchesStructure,
-    )
-import transaction
+)
 from zope.component import getUtility
 from zope.security.interfaces import Unauthorized
 from zope.security.proxy import removeSecurityProxy
@@ -44,21 +41,21 @@ from lp.buildmaster.enums import (
     BuildBaseImageType,
     BuildQueueStatus,
     BuildStatus,
-    )
+)
 from lp.buildmaster.interfaces.buildqueue import IBuildQueue
 from lp.buildmaster.interfaces.processor import (
     IProcessorSet,
     ProcessorNotFound,
-    )
+)
 from lp.buildmaster.model.buildfarmjob import BuildFarmJob
 from lp.buildmaster.model.buildqueue import BuildQueue
 from lp.charms.interfaces.charmrecipe import (
-    BadCharmRecipeSearchContext,
-    CannotAuthorizeCharmhubUploads,
     CHARM_RECIPE_ALLOW_CREATE,
     CHARM_RECIPE_BUILD_DISTRIBUTION,
     CHARM_RECIPE_PRIVATE_FEATURE_FLAG,
     CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG,
+    BadCharmRecipeSearchContext,
+    CannotAuthorizeCharmhubUploads,
     CharmRecipeBuildAlreadyPending,
     CharmRecipeBuildDisallowedArchitecture,
     CharmRecipeBuildRequestStatus,
@@ -68,37 +65,31 @@ from lp.charms.interfaces.charmrecipe import (
     ICharmRecipeSet,
     ICharmRecipeView,
     NoSourceForCharmRecipe,
-    )
+)
 from lp.charms.interfaces.charmrecipebuild import (
     ICharmRecipeBuild,
     ICharmRecipeBuildSet,
-    )
+)
 from lp.charms.interfaces.charmrecipebuildjob import ICharmhubUploadJobSource
 from lp.charms.interfaces.charmrecipejob import (
     ICharmRecipeRequestBuildsJobSource,
-    )
+)
 from lp.charms.model.charmrecipe import CharmRecipeSet
 from lp.charms.model.charmrecipebuild import CharmFile
 from lp.charms.model.charmrecipebuildjob import CharmRecipeBuildJob
 from lp.charms.model.charmrecipejob import CharmRecipeJob
 from lp.code.errors import GitRepositoryBlobNotFound
 from lp.code.tests.helpers import GitHostingFixture
-from lp.registry.enums import (
-    PersonVisibility,
-    TeamMembershipPolicy,
-    )
+from lp.registry.enums import PersonVisibility, TeamMembershipPolicy
 from lp.registry.interfaces.series import SeriesStatus
 from lp.services.config import config
 from lp.services.crypto.interfaces import IEncryptedContainer
-from lp.services.database.constants import (
-    ONE_DAY_AGO,
-    UTC_NOW,
-    )
+from lp.services.database.constants import ONE_DAY_AGO, UTC_NOW
 from lp.services.database.interfaces import IStore
 from lp.services.database.sqlbase import (
     flush_database_caches,
     get_transaction_timestamp,
-    )
+)
 from lp.services.features.testing import FeatureFixture
 from lp.services.job.interfaces.job import JobStatus
 from lp.services.job.runner import JobRunner
@@ -108,26 +99,23 @@ from lp.services.webapp.publisher import canonical_url
 from lp.services.webapp.snapshot import notify_modified
 from lp.services.webhooks.testing import LogsScheduledWebhooks
 from lp.testing import (
-    admin_logged_in,
     ANONYMOUS,
+    StormStatementRecorder,
+    TestCaseWithFactory,
+    admin_logged_in,
     api_url,
     login,
     logout,
     person_logged_in,
     record_two_runs,
-    StormStatementRecorder,
-    TestCaseWithFactory,
-    )
+)
 from lp.testing.dbuser import dbuser
 from lp.testing.layers import (
     DatabaseFunctionalLayer,
     LaunchpadFunctionalLayer,
     LaunchpadZopelessLayer,
-    )
-from lp.testing.matchers import (
-    DoesNotSnapshot,
-    HasQueryCount,
-    )
+)
+from lp.testing.matchers import DoesNotSnapshot, HasQueryCount
 from lp.testing.pages import webservice_for_person
 
 
@@ -138,15 +126,18 @@ class TestCharmRecipeFeatureFlags(TestCaseWithFactory):
     def test_feature_flag_disabled(self):
         # Without a feature flag, we wil not create any charm recipes.
         self.assertRaises(
-            CharmRecipeFeatureDisabled, self.factory.makeCharmRecipe)
+            CharmRecipeFeatureDisabled, self.factory.makeCharmRecipe
+        )
 
     def test_private_feature_flag_disabled(self):
         # Without a private feature flag, we wil not create new private
         # charm recipes.
         self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
         self.assertRaises(
-            CharmRecipePrivateFeatureDisabled, self.factory.makeCharmRecipe,
-            information_type=InformationType.PROPRIETARY)
+            CharmRecipePrivateFeatureDisabled,
+            self.factory.makeCharmRecipe,
+            information_type=InformationType.PROPRIETARY,
+        )
 
 
 class TestCharmRecipe(TestCaseWithFactory):
@@ -167,17 +158,25 @@ class TestCharmRecipe(TestCaseWithFactory):
         # CharmRecipe objects have an informative __repr__.
         recipe = self.factory.makeCharmRecipe()
         self.assertEqual(
-            "<CharmRecipe ~%s/%s/+charm/%s>" % (
-                recipe.owner.name, recipe.project.name, recipe.name),
-            repr(recipe))
+            "<CharmRecipe ~%s/%s/+charm/%s>"
+            % (recipe.owner.name, recipe.project.name, recipe.name),
+            repr(recipe),
+        )
 
     def test_avoids_problematic_snapshots(self):
         self.assertThat(
             self.factory.makeCharmRecipe(),
             DoesNotSnapshot(
-                ["pending_build_requests", "failed_build_requests",
-                 "builds", "completed_builds", "pending_builds"],
-                ICharmRecipeView))
+                [
+                    "pending_build_requests",
+                    "failed_build_requests",
+                    "builds",
+                    "completed_builds",
+                    "pending_builds",
+                ],
+                ICharmRecipeView,
+            ),
+        )
 
     def test_initial_date_last_modified(self):
         # The initial value of date_last_modified is date_created.
@@ -191,14 +190,16 @@ class TestCharmRecipe(TestCaseWithFactory):
         with notify_modified(removeSecurityProxy(recipe), ["name"]):
             pass
         self.assertSqlAttributeEqualsDate(
-            recipe, "date_last_modified", UTC_NOW)
+            recipe, "date_last_modified", UTC_NOW
+        )
 
     def test__default_distribution_default(self):
         # If the CHARM_RECIPE_BUILD_DISTRIBUTION feature rule is not set, we
         # default to Ubuntu.
         recipe = self.factory.makeCharmRecipe()
         self.assertEqual(
-            "ubuntu", removeSecurityProxy(recipe)._default_distribution.name)
+            "ubuntu", removeSecurityProxy(recipe)._default_distribution.name
+        )
 
     def test__default_distribution_feature_rule(self):
         # If the CHARM_RECIPE_BUILD_DISTRIBUTION feature rule is set, we
@@ -208,8 +209,8 @@ class TestCharmRecipe(TestCaseWithFactory):
         recipe = self.factory.makeCharmRecipe()
         with FeatureFixture({CHARM_RECIPE_BUILD_DISTRIBUTION: distro_name}):
             self.assertEqual(
-                distribution,
-                removeSecurityProxy(recipe)._default_distribution)
+                distribution, removeSecurityProxy(recipe)._default_distribution
+            )
 
     def test__default_distribution_feature_rule_nonexistent(self):
         # If we mistakenly set the rule to a non-existent distribution,
@@ -217,11 +218,16 @@ class TestCharmRecipe(TestCaseWithFactory):
         recipe = self.factory.makeCharmRecipe()
         with FeatureFixture({CHARM_RECIPE_BUILD_DISTRIBUTION: "nonexistent"}):
             expected_msg = (
-                "'nonexistent' is not a valid value for feature rule '%s'" %
-                CHARM_RECIPE_BUILD_DISTRIBUTION)
+                "'nonexistent' is not a valid value for feature rule '%s'"
+                % CHARM_RECIPE_BUILD_DISTRIBUTION
+            )
             self.assertRaisesWithContent(
-                ValueError, expected_msg,
-                getattr, removeSecurityProxy(recipe), "_default_distribution")
+                ValueError,
+                expected_msg,
+                getattr,
+                removeSecurityProxy(recipe),
+                "_default_distribution",
+            )
 
     def test__default_distro_series_feature_rule(self):
         # If the appropriate per-distribution feature rule is set, we
@@ -230,17 +236,21 @@ class TestCharmRecipe(TestCaseWithFactory):
         distribution = self.factory.makeDistribution(name=distro_name)
         distro_series_name = "myseries"
         distro_series = self.factory.makeDistroSeries(
-            distribution=distribution, name=distro_series_name)
+            distribution=distribution, name=distro_series_name
+        )
         self.factory.makeDistroSeries(distribution=distribution)
         recipe = self.factory.makeCharmRecipe()
-        with FeatureFixture({
+        with FeatureFixture(
+            {
                 CHARM_RECIPE_BUILD_DISTRIBUTION: distro_name,
-                "charm.default_build_series.%s" % distro_name: (
-                    distro_series_name),
-                }):
+                "charm.default_build_series.%s"
+                % distro_name: (distro_series_name),
+            }
+        ):
             self.assertEqual(
                 distro_series,
-                removeSecurityProxy(recipe)._default_distro_series)
+                removeSecurityProxy(recipe)._default_distro_series,
+            )
 
     def test__default_distro_series_no_feature_rule(self):
         # If the appropriate per-distribution feature rule is not set, we
@@ -248,40 +258,52 @@ class TestCharmRecipe(TestCaseWithFactory):
         distro_name = "mydistro"
         distribution = self.factory.makeDistribution(name=distro_name)
         self.factory.makeDistroSeries(
-            distribution=distribution, status=SeriesStatus.SUPPORTED)
+            distribution=distribution, status=SeriesStatus.SUPPORTED
+        )
         current_series = self.factory.makeDistroSeries(
-            distribution=distribution, status=SeriesStatus.DEVELOPMENT)
+            distribution=distribution, status=SeriesStatus.DEVELOPMENT
+        )
         recipe = self.factory.makeCharmRecipe()
         with FeatureFixture({CHARM_RECIPE_BUILD_DISTRIBUTION: distro_name}):
             self.assertEqual(
                 current_series,
-                removeSecurityProxy(recipe)._default_distro_series)
-
-    def makeBuildableDistroArchSeries(self, architecturetag=None,
-                                      processor=None,
-                                      supports_virtualized=True,
-                                      supports_nonvirtualized=True, **kwargs):
+                removeSecurityProxy(recipe)._default_distro_series,
+            )
+
+    def makeBuildableDistroArchSeries(
+        self,
+        architecturetag=None,
+        processor=None,
+        supports_virtualized=True,
+        supports_nonvirtualized=True,
+        **kwargs
+    ):
         if architecturetag is None:
             architecturetag = self.factory.getUniqueUnicode("arch")
         if processor is None:
             try:
                 processor = getUtility(IProcessorSet).getByName(
-                    architecturetag)
+                    architecturetag
+                )
             except ProcessorNotFound:
                 processor = self.factory.makeProcessor(
                     name=architecturetag,
                     supports_virtualized=supports_virtualized,
-                    supports_nonvirtualized=supports_nonvirtualized)
+                    supports_nonvirtualized=supports_nonvirtualized,
+                )
         das = self.factory.makeDistroArchSeries(
-            architecturetag=architecturetag, processor=processor, **kwargs)
+            architecturetag=architecturetag, processor=processor, **kwargs
+        )
         # Add both a chroot and a LXD image to test that
         # getAllowedArchitectures doesn't get confused by multiple
         # PocketChroot rows for a single DistroArchSeries.
         fake_chroot = self.factory.makeLibraryFileAlias(
-            filename="fake_chroot.tar.gz", db_only=True)
+            filename="fake_chroot.tar.gz", db_only=True
+        )
         das.addOrUpdateChroot(fake_chroot)
         fake_lxd = self.factory.makeLibraryFileAlias(
-            filename="fake_lxd.tar.gz", db_only=True)
+            filename="fake_lxd.tar.gz", db_only=True
+        )
         das.addOrUpdateChroot(fake_lxd, image_type=BuildBaseImageType.LXD)
         return das
 
@@ -292,18 +314,22 @@ class TestCharmRecipe(TestCaseWithFactory):
         build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
         build = recipe.requestBuild(build_request, das)
         self.assertTrue(ICharmRecipeBuild.providedBy(build))
-        self.assertThat(build, MatchesStructure(
-            requester=Equals(recipe.owner.teamowner),
-            distro_arch_series=Equals(das),
-            channels=Is(None),
-            status=Equals(BuildStatus.NEEDSBUILD),
-            ))
+        self.assertThat(
+            build,
+            MatchesStructure(
+                requester=Equals(recipe.owner.teamowner),
+                distro_arch_series=Equals(das),
+                channels=Is(None),
+                status=Equals(BuildStatus.NEEDSBUILD),
+            ),
+        )
         store = Store.of(build)
         store.flush()
         build_queue = store.find(
             BuildQueue,
-            BuildQueue._build_farm_job_id ==
-                removeSecurityProxy(build).build_farm_job_id).one()
+            BuildQueue._build_farm_job_id
+            == removeSecurityProxy(build).build_farm_job_id,
+        ).one()
         self.assertProvides(build_queue, IBuildQueue)
         self.assertEqual(recipe.require_virtualized, build_queue.virtualized)
         self.assertEqual(BuildQueueStatus.WAITING, build_queue.status)
@@ -324,7 +350,8 @@ class TestCharmRecipe(TestCaseWithFactory):
         das = self.makeBuildableDistroArchSeries()
         build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
         build = recipe.requestBuild(
-            build_request, das, channels={"charmcraft": "edge"})
+            build_request, das, channels={"charmcraft": "edge"}
+        )
         self.assertEqual({"charmcraft": "edge"}, build.channels)
 
     def test_requestBuild_rejects_repeats(self):
@@ -333,24 +360,37 @@ class TestCharmRecipe(TestCaseWithFactory):
         distro_series = self.factory.makeDistroSeries()
         arches = [
             self.makeBuildableDistroArchSeries(distroseries=distro_series)
-            for _ in range(2)]
+            for _ in range(2)
+        ]
         build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
         old_build = recipe.requestBuild(build_request, arches[0])
         self.assertRaises(
-            CharmRecipeBuildAlreadyPending, recipe.requestBuild,
-            build_request, arches[0])
+            CharmRecipeBuildAlreadyPending,
+            recipe.requestBuild,
+            build_request,
+            arches[0],
+        )
         # We can build for a different distroarchseries.
         recipe.requestBuild(build_request, arches[1])
         # channels=None and channels={} are treated as equivalent, but
         # anything else allows a new build.
         self.assertRaises(
-            CharmRecipeBuildAlreadyPending, recipe.requestBuild,
-            build_request, arches[0], channels={})
+            CharmRecipeBuildAlreadyPending,
+            recipe.requestBuild,
+            build_request,
+            arches[0],
+            channels={},
+        )
         recipe.requestBuild(
-            build_request, arches[0], channels={"core": "edge"})
+            build_request, arches[0], channels={"core": "edge"}
+        )
         self.assertRaises(
-            CharmRecipeBuildAlreadyPending, recipe.requestBuild,
-            build_request, arches[0], channels={"core": "edge"})
+            CharmRecipeBuildAlreadyPending,
+            recipe.requestBuild,
+            build_request,
+            arches[0],
+            channels={"core": "edge"},
+        )
         # Changing the status of the old build allows a new build.
         old_build.updateStatus(BuildStatus.BUILDING)
         old_build.updateStatus(BuildStatus.FULLYBUILT)
@@ -364,20 +404,24 @@ class TestCharmRecipe(TestCaseWithFactory):
         dases = {}
         for proc_nonvirt in True, False:
             das = self.makeBuildableDistroArchSeries(
-                distroseries=distro_series, supports_virtualized=True,
-                supports_nonvirtualized=proc_nonvirt)
+                distroseries=distro_series,
+                supports_virtualized=True,
+                supports_nonvirtualized=proc_nonvirt,
+            )
             dases[proc_nonvirt] = das
         for proc_nonvirt, recipe_virt, build_virt in (
-                (True, False, False),
-                (True, True, True),
-                (False, False, True),
-                (False, True, True),
-                ):
+            (True, False, False),
+            (True, True, True),
+            (False, False, True),
+            (False, True, True),
+        ):
             das = dases[proc_nonvirt]
             recipe = self.factory.makeCharmRecipe(
-                require_virtualized=recipe_virt)
+                require_virtualized=recipe_virt
+            )
             build_request = self.factory.makeCharmRecipeBuildRequest(
-                recipe=recipe)
+                recipe=recipe
+            )
             build = recipe.requestBuild(build_request, das)
             self.assertEqual(build_virt, build.virtualized)
 
@@ -387,55 +431,78 @@ class TestCharmRecipe(TestCaseWithFactory):
         recipe = self.factory.makeCharmRecipe()
         distro_series = self.factory.makeDistroSeries()
         das = self.makeBuildableDistroArchSeries(
-            distroseries=distro_series, supports_virtualized=False,
-            supports_nonvirtualized=True)
+            distroseries=distro_series,
+            supports_virtualized=False,
+            supports_nonvirtualized=True,
+        )
         build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
         self.assertRaises(
-            CharmRecipeBuildDisallowedArchitecture, recipe.requestBuild,
-            build_request, das)
+            CharmRecipeBuildDisallowedArchitecture,
+            recipe.requestBuild,
+            build_request,
+            das,
+        )
         with admin_logged_in():
             recipe.require_virtualized = False
         recipe.requestBuild(build_request, das)
 
     def test_requestBuild_triggers_webhooks(self):
         # Requesting a build triggers webhooks.
-        self.useFixture(FeatureFixture({
-            CHARM_RECIPE_ALLOW_CREATE: "on",
-            CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
-            }))
+        self.useFixture(
+            FeatureFixture(
+                {
+                    CHARM_RECIPE_ALLOW_CREATE: "on",
+                    CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
+                }
+            )
+        )
         logger = self.useFixture(FakeLogger())
         recipe = self.factory.makeCharmRecipe()
         das = self.makeBuildableDistroArchSeries()
         build_request = self.factory.makeCharmRecipeBuildRequest(recipe=recipe)
         hook = self.factory.makeWebhook(
-            target=recipe, event_types=["charm-recipe:build:0.1"])
+            target=recipe, event_types=["charm-recipe:build:0.1"]
+        )
         build = recipe.requestBuild(build_request, das)
         expected_payload = {
             "recipe_build": Equals(
-                canonical_url(build, force_local_path=True)),
+                canonical_url(build, force_local_path=True)
+            ),
             "action": Equals("created"),
             "recipe": Equals(canonical_url(recipe, force_local_path=True)),
             "build_request": Equals(
-                canonical_url(build_request, force_local_path=True)),
+                canonical_url(build_request, force_local_path=True)
+            ),
             "status": Equals("Needs building"),
             "store_upload_status": Equals("Unscheduled"),
-            }
+        }
         with person_logged_in(recipe.owner):
             delivery = hook.deliveries.one()
             self.assertThat(
-                delivery, MatchesStructure(
+                delivery,
+                MatchesStructure(
                     event_type=Equals("charm-recipe:build:0.1"),
-                    payload=MatchesDict(expected_payload)))
+                    payload=MatchesDict(expected_payload),
+                ),
+            )
             with dbuser(config.IWebhookDeliveryJobSource.dbuser):
                 self.assertEqual(
-                    "<WebhookDeliveryJob for webhook %d on %r>" % (
-                        hook.id, hook.target),
-                    repr(delivery))
+                    "<WebhookDeliveryJob for webhook %d on %r>"
+                    % (hook.id, hook.target),
+                    repr(delivery),
+                )
                 self.assertThat(
                     logger.output,
-                    LogsScheduledWebhooks([
-                        (hook, "charm-recipe:build:0.1",
-                         MatchesDict(expected_payload))]))
+                    LogsScheduledWebhooks(
+                        [
+                            (
+                                hook,
+                                "charm-recipe:build:0.1",
+                                MatchesDict(expected_payload),
+                            )
+                        ]
+                    ),
+                )
 
     def test_requestBuilds(self):
         # requestBuilds schedules a job and returns a corresponding
@@ -444,22 +511,30 @@ class TestCharmRecipe(TestCaseWithFactory):
         now = get_transaction_timestamp(IStore(recipe))
         with person_logged_in(recipe.owner.teamowner):
             request = recipe.requestBuilds(recipe.owner.teamowner)
-        self.assertThat(request, MatchesStructure(
-            date_requested=Equals(now),
-            date_finished=Is(None),
-            recipe=Equals(recipe),
-            status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-            error_message=Is(None),
-            channels=Is(None),
-            architectures=Is(None)))
+        self.assertThat(
+            request,
+            MatchesStructure(
+                date_requested=Equals(now),
+                date_finished=Is(None),
+                recipe=Equals(recipe),
+                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                error_message=Is(None),
+                channels=Is(None),
+                architectures=Is(None),
+            ),
+        )
         [job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
-        self.assertThat(job, MatchesStructure(
-            job_id=Equals(request.id),
-            job=MatchesStructure.byEquality(status=JobStatus.WAITING),
-            recipe=Equals(recipe),
-            requester=Equals(recipe.owner.teamowner),
-            channels=Is(None),
-            architectures=Is(None)))
+        self.assertThat(
+            job,
+            MatchesStructure(
+                job_id=Equals(request.id),
+                job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+                recipe=Equals(recipe),
+                requester=Equals(recipe.owner.teamowner),
+                channels=Is(None),
+                architectures=Is(None),
+            ),
+        )
 
     def test_requestBuilds_with_channels(self):
         # If asked to build using particular snap channels, requestBuilds
@@ -468,23 +543,32 @@ class TestCharmRecipe(TestCaseWithFactory):
         now = get_transaction_timestamp(IStore(recipe))
         with person_logged_in(recipe.owner.teamowner):
             request = recipe.requestBuilds(
-                recipe.owner.teamowner, channels={"charmcraft": "edge"})
-        self.assertThat(request, MatchesStructure(
-            date_requested=Equals(now),
-            date_finished=Is(None),
-            recipe=Equals(recipe),
-            status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-            error_message=Is(None),
-            channels=MatchesDict({"charmcraft": Equals("edge")}),
-            architectures=Is(None)))
+                recipe.owner.teamowner, channels={"charmcraft": "edge"}
+            )
+        self.assertThat(
+            request,
+            MatchesStructure(
+                date_requested=Equals(now),
+                date_finished=Is(None),
+                recipe=Equals(recipe),
+                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                error_message=Is(None),
+                channels=MatchesDict({"charmcraft": Equals("edge")}),
+                architectures=Is(None),
+            ),
+        )
         [job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
-        self.assertThat(job, MatchesStructure(
-            job_id=Equals(request.id),
-            job=MatchesStructure.byEquality(status=JobStatus.WAITING),
-            recipe=Equals(recipe),
-            requester=Equals(recipe.owner.teamowner),
-            channels=Equals({"charmcraft": "edge"}),
-            architectures=Is(None)))
+        self.assertThat(
+            job,
+            MatchesStructure(
+                job_id=Equals(request.id),
+                job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+                recipe=Equals(recipe),
+                requester=Equals(recipe.owner.teamowner),
+                channels=Equals({"charmcraft": "edge"}),
+                architectures=Is(None),
+            ),
+        )
 
     def test_requestBuilds_with_architectures(self):
         # If asked to build for particular architectures, requestBuilds
@@ -493,130 +577,183 @@ class TestCharmRecipe(TestCaseWithFactory):
         now = get_transaction_timestamp(IStore(recipe))
         with person_logged_in(recipe.owner.teamowner):
             request = recipe.requestBuilds(
-                recipe.owner.teamowner, architectures={"amd64", "i386"})
-        self.assertThat(request, MatchesStructure(
-            date_requested=Equals(now),
-            date_finished=Is(None),
-            recipe=Equals(recipe),
-            status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-            error_message=Is(None),
-            channels=Is(None),
-            architectures=MatchesSetwise(Equals("amd64"), Equals("i386"))))
+                recipe.owner.teamowner, architectures={"amd64", "i386"}
+            )
+        self.assertThat(
+            request,
+            MatchesStructure(
+                date_requested=Equals(now),
+                date_finished=Is(None),
+                recipe=Equals(recipe),
+                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                error_message=Is(None),
+                channels=Is(None),
+                architectures=MatchesSetwise(Equals("amd64"), Equals("i386")),
+            ),
+        )
         [job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
-        self.assertThat(job, MatchesStructure(
-            job_id=Equals(request.id),
-            job=MatchesStructure.byEquality(status=JobStatus.WAITING),
-            recipe=Equals(recipe),
-            requester=Equals(recipe.owner.teamowner),
-            channels=Is(None),
-            architectures=MatchesSetwise(Equals("amd64"), Equals("i386"))))
-
-    def makeRequestBuildsJob(self, distro_series_version, arch_tags,
-                             git_ref=None):
+        self.assertThat(
+            job,
+            MatchesStructure(
+                job_id=Equals(request.id),
+                job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+                recipe=Equals(recipe),
+                requester=Equals(recipe.owner.teamowner),
+                channels=Is(None),
+                architectures=MatchesSetwise(Equals("amd64"), Equals("i386")),
+            ),
+        )
+
+    def makeRequestBuildsJob(
+        self, distro_series_version, arch_tags, git_ref=None
+    ):
         recipe = self.factory.makeCharmRecipe(git_ref=git_ref)
         distro_series = self.factory.makeDistroSeries(
             distribution=getUtility(ILaunchpadCelebrities).ubuntu,
-            version=distro_series_version)
+            version=distro_series_version,
+        )
         for arch_tag in arch_tags:
             self.makeBuildableDistroArchSeries(
-                distroseries=distro_series, architecturetag=arch_tag)
+                distroseries=distro_series, architecturetag=arch_tag
+            )
         return getUtility(ICharmRecipeRequestBuildsJobSource).create(
-            recipe, recipe.owner.teamowner, {"charmcraft": "edge"})
-
-    def assertRequestedBuildsMatch(self, builds, job, distro_series_version,
-                                   arch_tags, channels):
-        self.assertThat(builds, MatchesSetwise(
-            *(MatchesStructure(
-                requester=Equals(job.requester),
-                recipe=Equals(job.recipe),
-                distro_arch_series=MatchesStructure(
-                    distroseries=MatchesStructure.byEquality(
-                        version=distro_series_version),
-                    architecturetag=Equals(arch_tag)),
-                channels=Equals(channels))
-              for arch_tag in arch_tags)))
+            recipe, recipe.owner.teamowner, {"charmcraft": "edge"}
+        )
+
+    def assertRequestedBuildsMatch(
+        self, builds, job, distro_series_version, arch_tags, channels
+    ):
+        self.assertThat(
+            builds,
+            MatchesSetwise(
+                *(
+                    MatchesStructure(
+                        requester=Equals(job.requester),
+                        recipe=Equals(job.recipe),
+                        distro_arch_series=MatchesStructure(
+                            distroseries=MatchesStructure.byEquality(
+                                version=distro_series_version
+                            ),
+                            architecturetag=Equals(arch_tag),
+                        ),
+                        channels=Equals(channels),
+                    )
+                    for arch_tag in arch_tags
+                )
+            ),
+        )
 
     def test_requestBuildsFromJob_restricts_explicit_list(self):
         # requestBuildsFromJob limits builds targeted at an explicit list of
         # architectures to those allowed for the recipe.
-        self.useFixture(GitHostingFixture(blob=dedent("""\
-            bases:
-              - build-on:
-                  - name: ubuntu
-                    channel: "20.04"
-                    architectures: [sparc]
-              - build-on:
-                  - name: ubuntu
-                    channel: "20.04"
-                    architectures: [i386]
-              - build-on:
-                  - name: ubuntu
-                    channel: "20.04"
-                    architectures: [avr]
-            """)))
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    bases:
+                      - build-on:
+                          - name: ubuntu
+                            channel: "20.04"
+                            architectures: [sparc]
+                      - build-on:
+                          - name: ubuntu
+                            channel: "20.04"
+                            architectures: [i386]
+                      - build-on:
+                          - name: ubuntu
+                            channel: "20.04"
+                            architectures: [avr]
+                    """
+                )
+            )
+        )
         job = self.makeRequestBuildsJob("20.04", ["sparc", "avr", "mips64el"])
         self.assertEqual(
-            get_transaction_timestamp(IStore(job.recipe)), job.date_created)
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
         transaction.commit()
         with person_logged_in(job.requester):
             builds = job.recipe.requestBuildsFromJob(
-                job.build_request, channels=removeSecurityProxy(job.channels))
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
         self.assertRequestedBuildsMatch(
-            builds, job, "20.04", ["sparc", "avr"], job.channels)
+            builds, job, "20.04", ["sparc", "avr"], job.channels
+        )
 
     def test_requestBuildsFromJob_no_explicit_bases(self):
         # If the recipe doesn't specify any bases, requestBuildsFromJob
         # requests builds for all configured architectures for the default
         # series.
-        self.useFixture(FeatureFixture({
-            CHARM_RECIPE_ALLOW_CREATE: "on",
-            CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
-            "charm.default_build_series.ubuntu": "20.04",
-            }))
+        self.useFixture(
+            FeatureFixture(
+                {
+                    CHARM_RECIPE_ALLOW_CREATE: "on",
+                    CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
+                    "charm.default_build_series.ubuntu": "20.04",
+                }
+            )
+        )
         self.useFixture(GitHostingFixture(blob="name: foo\n"))
         old_distro_series = self.factory.makeDistroSeries(
             distribution=getUtility(ILaunchpadCelebrities).ubuntu,
-            version="18.04")
+            version="18.04",
+        )
         for arch_tag in ("mips64el", "riscv64"):
             self.makeBuildableDistroArchSeries(
-                distroseries=old_distro_series, architecturetag=arch_tag)
+                distroseries=old_distro_series, architecturetag=arch_tag
+            )
         job = self.makeRequestBuildsJob("20.04", ["mips64el", "riscv64"])
         self.assertEqual(
-            get_transaction_timestamp(IStore(job.recipe)), job.date_created)
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
         transaction.commit()
         with person_logged_in(job.requester):
             builds = job.recipe.requestBuildsFromJob(
-                job.build_request, channels=removeSecurityProxy(job.channels))
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
         self.assertRequestedBuildsMatch(
-            builds, job, "20.04", ["mips64el", "riscv64"], job.channels)
+            builds, job, "20.04", ["mips64el", "riscv64"], job.channels
+        )
 
     def test_requestBuildsFromJob_no_charmcraft_yaml(self):
         # If the recipe has no charmcraft.yaml file, requestBuildsFromJob
         # treats this as equivalent to a charmcraft.yaml file that doesn't
         # specify any bases: that is, it requests builds for all configured
         # architectures for the default series.
-        self.useFixture(FeatureFixture({
-            CHARM_RECIPE_ALLOW_CREATE: "on",
-            CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
-            "charm.default_build_series.ubuntu": "20.04",
-            }))
-        self.useFixture(GitHostingFixture()).getBlob.failure = (
-            GitRepositoryBlobNotFound("placeholder", "charmcraft.yaml"))
+        self.useFixture(
+            FeatureFixture(
+                {
+                    CHARM_RECIPE_ALLOW_CREATE: "on",
+                    CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
+                    "charm.default_build_series.ubuntu": "20.04",
+                }
+            )
+        )
+        self.useFixture(
+            GitHostingFixture()
+        ).getBlob.failure = GitRepositoryBlobNotFound(
+            "placeholder", "charmcraft.yaml"
+        )
         old_distro_series = self.factory.makeDistroSeries(
             distribution=getUtility(ILaunchpadCelebrities).ubuntu,
-            version="18.04")
+            version="18.04",
+        )
         for arch_tag in ("mips64el", "riscv64"):
             self.makeBuildableDistroArchSeries(
-                distroseries=old_distro_series, architecturetag=arch_tag)
+                distroseries=old_distro_series, architecturetag=arch_tag
+            )
         job = self.makeRequestBuildsJob("20.04", ["mips64el", "riscv64"])
         self.assertEqual(
-            get_transaction_timestamp(IStore(job.recipe)), job.date_created)
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
         transaction.commit()
         with person_logged_in(job.requester):
             builds = job.recipe.requestBuildsFromJob(
-                job.build_request, channels=removeSecurityProxy(job.channels))
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
         self.assertRequestedBuildsMatch(
-            builds, job, "20.04", ["mips64el", "riscv64"], job.channels)
+            builds, job, "20.04", ["mips64el", "riscv64"], job.channels
+        )
 
     def test_requestBuildsFromJob_architectures_parameter(self):
         # If an explicit set of architectures was given as a parameter,
@@ -624,16 +761,21 @@ class TestCharmRecipe(TestCaseWithFactory):
         # when requesting builds.
         self.useFixture(GitHostingFixture(blob="name: foo\n"))
         job = self.makeRequestBuildsJob(
-            "20.04", ["avr", "mips64el", "riscv64"])
+            "20.04", ["avr", "mips64el", "riscv64"]
+        )
         self.assertEqual(
-            get_transaction_timestamp(IStore(job.recipe)), job.date_created)
+            get_transaction_timestamp(IStore(job.recipe)), job.date_created
+        )
         transaction.commit()
         with person_logged_in(job.requester):
             builds = job.recipe.requestBuildsFromJob(
-                job.build_request, channels=removeSecurityProxy(job.channels),
-                architectures={"avr", "riscv64"})
+                job.build_request,
+                channels=removeSecurityProxy(job.channels),
+                architectures={"avr", "riscv64"},
+            )
         self.assertRequestedBuildsMatch(
-            builds, job, "20.04", ["avr", "riscv64"], job.channels)
+            builds, job, "20.04", ["avr", "riscv64"], job.channels
+        )
 
     def test_requestBuildsFromJob_charm_base_architectures(self):
         # requestBuildsFromJob intersects the architectures supported by the
@@ -641,20 +783,25 @@ class TestCharmRecipe(TestCaseWithFactory):
         self.useFixture(GitHostingFixture(blob="name: foo\n"))
         job = self.makeRequestBuildsJob("20.04", ["sparc", "i386", "avr"])
         distroseries = getUtility(ILaunchpadCelebrities).ubuntu.getSeries(
-            "20.04")
+            "20.04"
+        )
         with admin_logged_in():
             self.factory.makeCharmBase(
                 distro_series=distroseries,
                 build_snap_channels={"charmcraft": "stable/launchpad-buildd"},
                 processors=[
                     distroseries[arch_tag].processor
-                    for arch_tag in ("sparc", "avr")])
+                    for arch_tag in ("sparc", "avr")
+                ],
+            )
         transaction.commit()
         with person_logged_in(job.requester):
             builds = job.recipe.requestBuildsFromJob(
-                job.build_request, channels=removeSecurityProxy(job.channels))
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
         self.assertRequestedBuildsMatch(
-            builds, job, "20.04", ["sparc", "avr"], job.channels)
+            builds, job, "20.04", ["sparc", "avr"], job.channels
+        )
 
     def test_requestBuildsFromJob_charm_base_build_channels_by_arch(self):
         # If the charm base declares different build channels for specific
@@ -663,72 +810,113 @@ class TestCharmRecipe(TestCaseWithFactory):
         self.useFixture(GitHostingFixture(blob="name: foo\n"))
         job = self.makeRequestBuildsJob("20.04", ["avr", "riscv64"])
         distroseries = getUtility(ILaunchpadCelebrities).ubuntu.getSeries(
-            "20.04")
+            "20.04"
+        )
         with admin_logged_in():
             self.factory.makeCharmBase(
                 distro_series=distroseries,
                 build_snap_channels={
                     "core20": "stable",
                     "_byarch": {"riscv64": {"core20": "candidate"}},
-                    })
+                },
+            )
         transaction.commit()
         with person_logged_in(job.requester):
             builds = job.recipe.requestBuildsFromJob(
-                job.build_request, channels=removeSecurityProxy(job.channels))
-        self.assertThat(builds, MatchesSetwise(
-            *(MatchesStructure(
-                requester=Equals(job.requester),
-                recipe=Equals(job.recipe),
-                distro_arch_series=MatchesStructure(
-                    distroseries=MatchesStructure.byEquality(version="20.04"),
-                    architecturetag=Equals(arch_tag)),
-                channels=Equals(channels))
-              for arch_tag, channels in (
-                  ("avr", {"charmcraft": "edge", "core20": "stable"}),
-                  ("riscv64", {"charmcraft": "edge", "core20": "candidate"}),
-                  ))))
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
+        self.assertThat(
+            builds,
+            MatchesSetwise(
+                *(
+                    MatchesStructure(
+                        requester=Equals(job.requester),
+                        recipe=Equals(job.recipe),
+                        distro_arch_series=MatchesStructure(
+                            distroseries=MatchesStructure.byEquality(
+                                version="20.04"
+                            ),
+                            architecturetag=Equals(arch_tag),
+                        ),
+                        channels=Equals(channels),
+                    )
+                    for arch_tag, channels in (
+                        ("avr", {"charmcraft": "edge", "core20": "stable"}),
+                        (
+                            "riscv64",
+                            {"charmcraft": "edge", "core20": "candidate"},
+                        ),
+                    )
+                )
+            ),
+        )
 
     def test_requestBuildsFromJob_triggers_webhooks(self):
         # requestBuildsFromJob triggers webhooks, and the payload includes a
         # link to the build request.
-        self.useFixture(FeatureFixture({
-            CHARM_RECIPE_ALLOW_CREATE: "on",
-            CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
-            "charm.default_build_series.ubuntu": "20.04",
-            CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
-            }))
+        self.useFixture(
+            FeatureFixture(
+                {
+                    CHARM_RECIPE_ALLOW_CREATE: "on",
+                    CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
+                    "charm.default_build_series.ubuntu": "20.04",
+                    CHARM_RECIPE_WEBHOOKS_FEATURE_FLAG: "on",
+                }
+            )
+        )
         self.useFixture(GitHostingFixture(blob="name: foo\n"))
         logger = self.useFixture(FakeLogger())
         job = self.makeRequestBuildsJob("20.04", ["mips64el", "riscv64"])
         hook = self.factory.makeWebhook(
-            target=job.recipe, event_types=["charm-recipe:build:0.1"])
+            target=job.recipe, event_types=["charm-recipe:build:0.1"]
+        )
         with person_logged_in(job.requester):
             builds = job.recipe.requestBuildsFromJob(
-                job.build_request, channels=removeSecurityProxy(job.channels))
+                job.build_request, channels=removeSecurityProxy(job.channels)
+            )
             self.assertEqual(2, len(builds))
             payload_matchers = [
-                MatchesDict({
-                    "recipe_build": Equals(canonical_url(
-                        build, force_local_path=True)),
-                    "action": Equals("created"),
-                    "recipe": Equals(canonical_url(
-                        job.recipe, force_local_path=True)),
-                    "build_request": Equals(canonical_url(
-                        job.build_request, force_local_path=True)),
-                    "status": Equals("Needs building"),
-                    "store_upload_status": Equals("Unscheduled"),
-                    })
-                for build in builds]
-            self.assertThat(hook.deliveries, MatchesSetwise(*(
-                MatchesStructure(
-                    event_type=Equals("charm-recipe:build:0.1"),
-                    payload=payload_matcher)
-                for payload_matcher in payload_matchers)))
+                MatchesDict(
+                    {
+                        "recipe_build": Equals(
+                            canonical_url(build, force_local_path=True)
+                        ),
+                        "action": Equals("created"),
+                        "recipe": Equals(
+                            canonical_url(job.recipe, force_local_path=True)
+                        ),
+                        "build_request": Equals(
+                            canonical_url(
+                                job.build_request, force_local_path=True
+                            )
+                        ),
+                        "status": Equals("Needs building"),
+                        "store_upload_status": Equals("Unscheduled"),
+                    }
+                )
+                for build in builds
+            ]
+            self.assertThat(
+                hook.deliveries,
+                MatchesSetwise(
+                    *(
+                        MatchesStructure(
+                            event_type=Equals("charm-recipe:build:0.1"),
+                            payload=payload_matcher,
+                        )
+                        for payload_matcher in payload_matchers
+                    )
+                ),
+            )
             self.assertThat(
                 logger.output,
-                LogsScheduledWebhooks([
-                    (hook, "charm-recipe:build:0.1", payload_matcher)
-                    for payload_matcher in payload_matchers]))
+                LogsScheduledWebhooks(
+                    [
+                        (hook, "charm-recipe:build:0.1", payload_matcher)
+                        for payload_matcher in payload_matchers
+                    ]
+                ),
+            )
 
     def test_requestAutoBuilds(self):
         # requestAutoBuilds creates a new build request with appropriate
@@ -737,59 +925,79 @@ class TestCharmRecipe(TestCaseWithFactory):
         now = get_transaction_timestamp(IStore(recipe))
         with person_logged_in(recipe.owner.teamowner):
             request = recipe.requestAutoBuilds()
-        self.assertThat(request, MatchesStructure(
-            date_requested=Equals(now),
-            date_finished=Is(None),
-            recipe=Equals(recipe),
-            status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-            error_message=Is(None),
-            channels=Is(None),
-            architectures=Is(None)))
+        self.assertThat(
+            request,
+            MatchesStructure(
+                date_requested=Equals(now),
+                date_finished=Is(None),
+                recipe=Equals(recipe),
+                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                error_message=Is(None),
+                channels=Is(None),
+                architectures=Is(None),
+            ),
+        )
         [job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
-        self.assertThat(job, MatchesStructure(
-            job_id=Equals(request.id),
-            job=MatchesStructure.byEquality(status=JobStatus.WAITING),
-            recipe=Equals(recipe),
-            requester=Equals(recipe.owner),
-            channels=Is(None),
-            architectures=Is(None)))
+        self.assertThat(
+            job,
+            MatchesStructure(
+                job_id=Equals(request.id),
+                job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+                recipe=Equals(recipe),
+                requester=Equals(recipe.owner),
+                channels=Is(None),
+                architectures=Is(None),
+            ),
+        )
 
     def test_requestAutoBuilds_channels(self):
         # requestAutoBuilds honours CharmRecipe.auto_build_channels.
         recipe = self.factory.makeCharmRecipe(
-            auto_build_channels={"charmcraft": "edge"})
+            auto_build_channels={"charmcraft": "edge"}
+        )
         now = get_transaction_timestamp(IStore(recipe))
         with person_logged_in(recipe.owner.teamowner):
             request = recipe.requestAutoBuilds()
-        self.assertThat(request, MatchesStructure(
-            date_requested=Equals(now),
-            date_finished=Is(None),
-            recipe=Equals(recipe),
-            status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-            error_message=Is(None),
-            channels=Equals({"charmcraft": "edge"}),
-            architectures=Is(None)))
+        self.assertThat(
+            request,
+            MatchesStructure(
+                date_requested=Equals(now),
+                date_finished=Is(None),
+                recipe=Equals(recipe),
+                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                error_message=Is(None),
+                channels=Equals({"charmcraft": "edge"}),
+                architectures=Is(None),
+            ),
+        )
         [job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
-        self.assertThat(job, MatchesStructure(
-            job_id=Equals(request.id),
-            job=MatchesStructure.byEquality(status=JobStatus.WAITING),
-            recipe=Equals(recipe),
-            requester=Equals(recipe.owner),
-            channels=Equals({"charmcraft": "edge"}),
-            architectures=Is(None)))
+        self.assertThat(
+            job,
+            MatchesStructure(
+                job_id=Equals(request.id),
+                job=MatchesStructure.byEquality(status=JobStatus.WAITING),
+                recipe=Equals(recipe),
+                requester=Equals(recipe.owner),
+                channels=Equals({"charmcraft": "edge"}),
+                architectures=Is(None),
+            ),
+        )
 
     def test_delete_without_builds(self):
         # A charm recipe with no builds can be deleted.
         owner = self.factory.makePerson()
         project = self.factory.makeProduct()
         recipe = self.factory.makeCharmRecipe(
-            registrant=owner, owner=owner, project=project, name="condemned")
+            registrant=owner, owner=owner, project=project, name="condemned"
+        )
         self.assertTrue(
-            getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
+            getUtility(ICharmRecipeSet).exists(owner, project, "condemned")
+        )
         with person_logged_in(recipe.owner):
             recipe.destroySelf()
         self.assertFalse(
-            getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
+            getUtility(ICharmRecipeSet).exists(owner, project, "condemned")
+        )
 
     def test_related_webhooks_deleted(self):
         owner = self.factory.makePerson()
@@ -811,54 +1019,69 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
         self.useFixture(FeatureFixture({CHARM_RECIPE_ALLOW_CREATE: "on"}))
         self.pushConfig("charms", charmhub_url="http://charmhub.example/";)
         self.pushConfig(
-            "launchpad", candid_service_root="https://candid.test/";)
+            "launchpad", candid_service_root="https://candid.test/";
+        )
 
     @responses.activate
     def assertBeginsAuthorization(self, recipe, **kwargs):
         root_macaroon = Macaroon(version=2)
         root_macaroon.add_third_party_caveat(
-            "https://candid.test/";, "", "identity")
+            "https://candid.test/";, "", "identity"
+        )
         root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens";,
-            json={"macaroon": root_macaroon_raw})
+            "POST",
+            "http://charmhub.example/v1/tokens";,
+            json={"macaroon": root_macaroon_raw},
+        )
         self.assertEqual(root_macaroon_raw, recipe.beginAuthorization())
-        self.assertThat(responses.calls, MatchesListwise([
-            MatchesStructure(
-                request=MatchesStructure(
-                    url=Equals("http://charmhub.example/v1/tokens";),
-                    method=Equals("POST"),
-                    body=AfterPreprocessing(
-                        lambda b: json.loads(b.decode()),
-                        Equals({
-                            "description": (
-                                "{} for launchpad.test".format(
-                                    recipe.store_name)),
-                            "packages": [
-                                {"type": "charm", "name": recipe.store_name},
-                                ],
-                            "permissions": [
-                                "package-manage-releases",
-                                "package-manage-revisions",
-                                "package-view-revisions",
-                                ],
-                            })))),
-            ]))
+        tokens_matcher = MatchesStructure(
+            url=Equals("http://charmhub.example/v1/tokens";),
+            method=Equals("POST"),
+            body=AfterPreprocessing(
+                lambda b: json.loads(b.decode()),
+                Equals(
+                    {
+                        "description": (
+                            "{} for launchpad.test".format(recipe.store_name)
+                        ),
+                        "packages": [
+                            {
+                                "type": "charm",
+                                "name": recipe.store_name,
+                            },
+                        ],
+                        "permissions": [
+                            "package-manage-releases",
+                            "package-manage-revisions",
+                            "package-view-revisions",
+                        ],
+                    }
+                ),
+            ),
+        )
+        self.assertThat(
+            responses.calls,
+            MatchesListwise([MatchesStructure(request=tokens_matcher)]),
+        )
         self.assertEqual({"root": root_macaroon_raw}, recipe.store_secrets)
 
     def test_beginAuthorization(self):
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name=self.factory.getUniqueUnicode())
+            store_upload=True, store_name=self.factory.getUniqueUnicode()
+        )
         with person_logged_in(recipe.registrant):
             self.assertBeginsAuthorization(recipe)
 
     def test_beginAuthorization_unauthorized(self):
         # A user without edit access cannot authorize charm recipe uploads.
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name=self.factory.getUniqueUnicode())
+            store_upload=True, store_name=self.factory.getUniqueUnicode()
+        )
         with person_logged_in(self.factory.makePerson()):
             self.assertRaises(
-                Unauthorized, getattr, recipe, "beginAuthorization")
+                Unauthorized, getattr, recipe, "beginAuthorization"
+            )
 
     @responses.activate
     def test_completeAuthorization(self):
@@ -866,56 +1089,84 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
         self.pushConfig(
             "charms",
             charmhub_secrets_public_key=base64.b64encode(
-                bytes(private_key.public_key)).decode())
+                bytes(private_key.public_key)
+            ).decode(),
+        )
         root_macaroon = Macaroon(version=2)
         root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
         unbound_discharge_macaroon = Macaroon(version=2)
         unbound_discharge_macaroon_raw = unbound_discharge_macaroon.serialize(
-            JsonSerializer())
+            JsonSerializer()
+        )
         discharge_macaroon_raw = root_macaroon.prepare_for_request(
-            unbound_discharge_macaroon).serialize(JsonSerializer())
+            unbound_discharge_macaroon
+        ).serialize(JsonSerializer())
         exchanged_macaroon = Macaroon(version=2)
         exchanged_macaroon_raw = exchanged_macaroon.serialize(JsonSerializer())
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens/exchange";,
-            json={"macaroon": exchanged_macaroon_raw})
+            "POST",
+            "http://charmhub.example/v1/tokens/exchange";,
+            json={"macaroon": exchanged_macaroon_raw},
+        )
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name=self.factory.getUniqueUnicode(),
-            store_secrets={"root": root_macaroon_raw})
+            store_upload=True,
+            store_name=self.factory.getUniqueUnicode(),
+            store_secrets={"root": root_macaroon_raw},
+        )
         with person_logged_in(recipe.registrant):
             recipe.completeAuthorization(unbound_discharge_macaroon_raw)
             self.pushConfig(
                 "charms",
                 charmhub_secrets_private_key=base64.b64encode(
-                    bytes(private_key)).decode())
+                    bytes(private_key)
+                ).decode(),
+            )
             container = getUtility(IEncryptedContainer, "charmhub-secrets")
-            self.assertThat(recipe.store_secrets, MatchesDict({
-                "exchanged_encrypted": AfterPreprocessing(
-                    lambda data: container.decrypt(data).decode(),
-                    Equals(exchanged_macaroon_raw)),
-                }))
-        self.assertThat(responses.calls, MatchesListwise([
-            MatchesStructure(
-                request=MatchesStructure(
-                    url=Equals("http://charmhub.example/v1/tokens/exchange";),
-                    method=Equals("POST"),
-                    headers=ContainsDict({
-                        "Macaroons": AfterPreprocessing(
-                            lambda v: json.loads(
-                                base64.b64decode(v.encode()).decode()),
-                            Equals([
-                                json.loads(m) for m in (
+            self.assertThat(
+                recipe.store_secrets,
+                MatchesDict(
+                    {
+                        "exchanged_encrypted": AfterPreprocessing(
+                            lambda data: container.decrypt(data).decode(),
+                            Equals(exchanged_macaroon_raw),
+                        ),
+                    }
+                ),
+            )
+        exchange_matcher = MatchesStructure(
+            url=Equals("http://charmhub.example/v1/tokens/exchange";),
+            method=Equals("POST"),
+            headers=ContainsDict(
+                {
+                    "Macaroons": AfterPreprocessing(
+                        lambda v: json.loads(
+                            base64.b64decode(v.encode()).decode()
+                        ),
+                        Equals(
+                            [
+                                json.loads(m)
+                                for m in (
                                     root_macaroon_raw,
-                                    discharge_macaroon_raw)])),
-                        }),
-                    body=AfterPreprocessing(
-                        lambda b: json.loads(b.decode()),
-                        Equals({})))),
-            ]))
+                                    discharge_macaroon_raw,
+                                )
+                            ]
+                        ),
+                    ),
+                }
+            ),
+            body=AfterPreprocessing(
+                lambda b: json.loads(b.decode()), Equals({})
+            ),
+        )
+        self.assertThat(
+            responses.calls,
+            MatchesListwise([MatchesStructure(request=exchange_matcher)]),
+        )
 
     def test_completeAuthorization_without_beginAuthorization(self):
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name=self.factory.getUniqueUnicode())
+            store_upload=True, store_name=self.factory.getUniqueUnicode()
+        )
         discharge_macaroon = Macaroon(version=2)
         with person_logged_in(recipe.registrant):
             self.assertRaisesWithContent(
@@ -923,27 +1174,35 @@ class TestCharmRecipeAuthorization(TestCaseWithFactory):
                 "beginAuthorization must be called before "
                 "completeAuthorization.",
                 recipe.completeAuthorization,
-                discharge_macaroon.serialize(JsonSerializer()))
+                discharge_macaroon.serialize(JsonSerializer()),
+            )
 
     def test_completeAuthorization_unauthorized(self):
         root_macaroon = Macaroon(version=2)
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name=self.factory.getUniqueUnicode(),
-            store_secrets={"root": root_macaroon.serialize(JsonSerializer())})
+            store_upload=True,
+            store_name=self.factory.getUniqueUnicode(),
+            store_secrets={"root": root_macaroon.serialize(JsonSerializer())},
+        )
         with person_logged_in(self.factory.makePerson()):
             self.assertRaises(
-                Unauthorized, getattr, recipe, "completeAuthorization")
+                Unauthorized, getattr, recipe, "completeAuthorization"
+            )
 
     def test_completeAuthorization_malformed_discharge_macaroon(self):
         root_macaroon = Macaroon(version=2)
         recipe = self.factory.makeCharmRecipe(
-            store_upload=True, store_name=self.factory.getUniqueUnicode(),
-            store_secrets={"root": root_macaroon.serialize(JsonSerializer())})
+            store_upload=True,
+            store_name=self.factory.getUniqueUnicode(),
+            store_secrets={"root": root_macaroon.serialize(JsonSerializer())},
+        )
         with person_logged_in(recipe.registrant):
             self.assertRaisesWithContent(
                 CannotAuthorizeCharmhubUploads,
                 "Discharge macaroon is invalid.",
-                recipe.completeAuthorization, "nonsense")
+                recipe.completeAuthorization,
+                "nonsense",
+            )
 
 
 class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
@@ -963,28 +1222,47 @@ class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
         distroseries = self.factory.makeDistroSeries()
         processor = self.factory.makeProcessor(supports_virtualized=True)
         das = self.factory.makeDistroArchSeries(
-            distroseries=distroseries, architecturetag=processor.name,
-            processor=processor)
+            distroseries=distroseries,
+            architecturetag=processor.name,
+            processor=processor,
+        )
         das.addOrUpdateChroot(
             self.factory.makeLibraryFileAlias(
-                filename="fake_chroot.tar.gz", db_only=True))
-        self.useFixture(GitHostingFixture(blob=dedent("""\
-            bases:
-              - build-on:
-                  - name: "%s"
-                    channel: "%s"
-                    architectures: [%s]
-            """ % (
-                distroseries.distribution.name, distroseries.name,
-                processor.name))))
+                filename="fake_chroot.tar.gz", db_only=True
+            )
+        )
+        self.useFixture(
+            GitHostingFixture(
+                blob=dedent(
+                    """\
+                    bases:
+                      - build-on:
+                          - name: "%s"
+                            channel: "%s"
+                            architectures: [%s]
+                    """
+                    % (
+                        distroseries.distribution.name,
+                        distroseries.name,
+                        processor.name,
+                    )
+                )
+            )
+        )
         [git_ref] = self.factory.makeGitRefs()
         condemned_recipe = self.factory.makeCharmRecipe(
-            registrant=owner, owner=owner, project=project, name="condemned",
-            git_ref=git_ref)
+            registrant=owner,
+            owner=owner,
+            project=project,
+            name="condemned",
+            git_ref=git_ref,
+        )
         other_recipe = self.factory.makeCharmRecipe(
-            registrant=owner, owner=owner, project=project, git_ref=git_ref)
+            registrant=owner, owner=owner, project=project, git_ref=git_ref
+        )
         self.assertTrue(
-            getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
+            getUtility(ICharmRecipeSet).exists(owner, project, "condemned")
+        )
         with person_logged_in(owner):
             requests = []
             jobs = []
@@ -999,10 +1277,12 @@ class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
             [other_build] = requests[1].builds
             charm_file = self.factory.makeCharmFile(build=build)
             other_charm_file = self.factory.makeCharmFile(build=other_build)
-            charm_build_job = getUtility(
-                ICharmhubUploadJobSource).create(build)
-            other_build_job = getUtility(
-                ICharmhubUploadJobSource).create(other_build)
+            charm_build_job = getUtility(ICharmhubUploadJobSource).create(
+                build
+            )
+            other_build_job = getUtility(ICharmhubUploadJobSource).create(
+                other_build
+            )
         store = Store.of(condemned_recipe)
         store.flush()
         job_ids = [job.job_id for job in jobs]
@@ -1017,7 +1297,8 @@ class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
         # The deleted recipe, its build requests, its builds and the build job
         # are gone.
         self.assertFalse(
-            getUtility(ICharmRecipeSet).exists(owner, project, "condemned"))
+            getUtility(ICharmRecipeSet).exists(owner, project, "condemned")
+        )
         self.assertIsNone(store.get(CharmRecipeJob, job_ids[0]))
         self.assertIsNone(getUtility(ICharmRecipeBuildSet).getByID(build_id))
         self.assertIsNone(store.get(BuildQueue, build_queue_id))
@@ -1026,16 +1307,20 @@ class TestCharmRecipeDeleteWithBuilds(TestCaseWithFactory):
         self.assertIsNone(store.get(CharmRecipeBuildJob, charm_build_job_id))
         # Unrelated build requests, build jobs and builds are still present.
         self.assertIsNotNone(
-            store.get(CharmRecipeBuildJob, other_build_job.job_id))
+            store.get(CharmRecipeBuildJob, other_build_job.job_id)
+        )
         self.assertEqual(
             removeSecurityProxy(jobs[1]).context,
-            store.get(CharmRecipeJob, job_ids[1]))
+            store.get(CharmRecipeJob, job_ids[1]),
+        )
         self.assertEqual(
             other_build,
-            getUtility(ICharmRecipeBuildSet).getByID(other_build.id))
+            getUtility(ICharmRecipeBuildSet).getByID(other_build.id),
+        )
         self.assertIsNotNone(other_build.buildqueue_record)
         self.assertIsNotNone(
-            store.get(CharmFile, removeSecurityProxy(other_charm_file).id))
+            store.get(CharmFile, removeSecurityProxy(other_charm_file).id)
+        )
 
 
 class TestCharmRecipeSet(TestCaseWithFactory):
@@ -1063,7 +1348,7 @@ class TestCharmRecipeSet(TestCaseWithFactory):
             "owner": self.factory.makeTeam(owner=registrant),
             "project": self.factory.makeProduct(),
             "name": self.factory.getUniqueUnicode("charm-name"),
-            }
+        }
         if git_ref is None:
             git_ref = self.factory.makeGitRefs()[0]
         components["git_ref"] = git_ref
@@ -1097,22 +1382,30 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         # fails.
         registrant = self.factory.makePerson()
         self.assertRaises(
-            NoSourceForCharmRecipe, getUtility(ICharmRecipeSet).new,
-            registrant, registrant, self.factory.makeProduct(),
-            self.factory.getUniqueUnicode("charm-name"))
+            NoSourceForCharmRecipe,
+            getUtility(ICharmRecipeSet).new,
+            registrant,
+            registrant,
+            self.factory.makeProduct(),
+            self.factory.getUniqueUnicode("charm-name"),
+        )
 
     def test_getByName(self):
         owner = self.factory.makePerson()
         project = self.factory.makeProduct()
         project_recipe = self.factory.makeCharmRecipe(
-            registrant=owner, owner=owner, project=project, name="proj-charm")
+            registrant=owner, owner=owner, project=project, name="proj-charm"
+        )
         self.factory.makeCharmRecipe(
-            registrant=owner, owner=owner, name="proj-charm")
+            registrant=owner, owner=owner, name="proj-charm"
+        )
 
         self.assertEqual(
             project_recipe,
             getUtility(ICharmRecipeSet).getByName(
-                owner, project, "proj-charm"))
+                owner, project, "proj-charm"
+            ),
+        )
 
     def test_findByOwner(self):
         # ICharmRecipeSet.findByOwner returns all charm recipes with the
@@ -1121,8 +1414,9 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         recipes = []
         for owner in owners:
             for i in range(2):
-                recipes.append(self.factory.makeCharmRecipe(
-                    registrant=owner, owner=owner))
+                recipes.append(
+                    self.factory.makeCharmRecipe(registrant=owner, owner=owner)
+                )
         recipe_set = getUtility(ICharmRecipeSet)
         self.assertContentEqual(recipes[:2], recipe_set.findByOwner(owners[0]))
         self.assertContentEqual(recipes[2:], recipe_set.findByOwner(owners[1]))
@@ -1133,15 +1427,18 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         owners = [self.factory.makePerson() for i in range(2)]
         recipes = []
         for owner in owners:
-            recipes.append(self.factory.makeCharmRecipe(
-                registrant=owner, owner=owner))
+            recipes.append(
+                self.factory.makeCharmRecipe(registrant=owner, owner=owner)
+            )
             [ref] = self.factory.makeGitRefs(owner=owner)
             recipes.append(self.factory.makeCharmRecipe(git_ref=ref))
         recipe_set = getUtility(ICharmRecipeSet)
         self.assertContentEqual(
-            recipes[:2], recipe_set.findByPerson(owners[0]))
+            recipes[:2], recipe_set.findByPerson(owners[0])
+        )
         self.assertContentEqual(
-            recipes[2:], recipe_set.findByPerson(owners[1]))
+            recipes[2:], recipe_set.findByPerson(owners[1])
+        )
 
     def test_findByProject(self):
         # ICharmRecipeSet.findByProject returns all charm recipes based on
@@ -1157,9 +1454,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         recipes.append(self.factory.makeCharmRecipe(git_ref=ref))
         recipe_set = getUtility(ICharmRecipeSet)
         self.assertContentEqual(
-            recipes[:2], recipe_set.findByProject(projects[0]))
+            recipes[:2], recipe_set.findByProject(projects[0])
+        )
         self.assertContentEqual(
-            recipes[2:4], recipe_set.findByProject(projects[1]))
+            recipes[2:4], recipe_set.findByProject(projects[1])
+        )
 
     def test_findByGitRepository(self):
         # ICharmRecipeSet.findByGitRepository returns all charm recipes with
@@ -1172,9 +1471,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
                 recipes.append(self.factory.makeCharmRecipe(git_ref=ref))
         recipe_set = getUtility(ICharmRecipeSet)
         self.assertContentEqual(
-            recipes[:2], recipe_set.findByGitRepository(repositories[0]))
+            recipes[:2], recipe_set.findByGitRepository(repositories[0])
+        )
         self.assertContentEqual(
-            recipes[2:], recipe_set.findByGitRepository(repositories[1]))
+            recipes[2:], recipe_set.findByGitRepository(repositories[1])
+        )
 
     def test_findByGitRepository_paths(self):
         # ICharmRecipeSet.findByGitRepository can restrict by reference
@@ -1187,16 +1488,21 @@ class TestCharmRecipeSet(TestCaseWithFactory):
                 recipes.append(self.factory.makeCharmRecipe(git_ref=ref))
         recipe_set = getUtility(ICharmRecipeSet)
         self.assertContentEqual(
-            [], recipe_set.findByGitRepository(repositories[0], paths=[]))
+            [], recipe_set.findByGitRepository(repositories[0], paths=[])
+        )
         self.assertContentEqual(
             [recipes[0]],
             recipe_set.findByGitRepository(
-                repositories[0], paths=[recipes[0].git_ref.path]))
+                repositories[0], paths=[recipes[0].git_ref.path]
+            ),
+        )
         self.assertContentEqual(
             recipes[:2],
             recipe_set.findByGitRepository(
                 repositories[0],
-                paths=[recipes[0].git_ref.path, recipes[1].git_ref.path]))
+                paths=[recipes[0].git_ref.path, recipes[1].git_ref.path],
+            ),
+        )
 
     def test_findByGitRef(self):
         # ICharmRecipeSet.findByGitRef returns all charm recipes with the
@@ -1205,8 +1511,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         refs = []
         recipes = []
         for repository in repositories:
-            refs.extend(self.factory.makeGitRefs(
-                paths=["refs/heads/master", "refs/heads/other"]))
+            refs.extend(
+                self.factory.makeGitRefs(
+                    paths=["refs/heads/master", "refs/heads/other"]
+                )
+            )
             recipes.append(self.factory.makeCharmRecipe(git_ref=refs[-2]))
             recipes.append(self.factory.makeCharmRecipe(git_ref=refs[-1]))
         recipe_set = getUtility(ICharmRecipeSet)
@@ -1219,33 +1528,47 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         person = self.factory.makePerson()
         project = self.factory.makeProduct()
         repository = self.factory.makeGitRepository(
-            owner=person, target=project)
+            owner=person, target=project
+        )
         refs = self.factory.makeGitRefs(
             repository=repository,
-            paths=["refs/heads/master", "refs/heads/other"])
+            paths=["refs/heads/master", "refs/heads/other"],
+        )
         other_repository = self.factory.makeGitRepository()
         other_refs = self.factory.makeGitRefs(
             repository=other_repository,
-            paths=["refs/heads/master", "refs/heads/other"])
+            paths=["refs/heads/master", "refs/heads/other"],
+        )
         recipes = []
         recipes.append(self.factory.makeCharmRecipe(git_ref=refs[0]))
         recipes.append(self.factory.makeCharmRecipe(git_ref=refs[1]))
-        recipes.append(self.factory.makeCharmRecipe(
-            registrant=person, owner=person, git_ref=other_refs[0]))
-        recipes.append(self.factory.makeCharmRecipe(
-            project=project, git_ref=other_refs[1]))
+        recipes.append(
+            self.factory.makeCharmRecipe(
+                registrant=person, owner=person, git_ref=other_refs[0]
+            )
+        )
+        recipes.append(
+            self.factory.makeCharmRecipe(
+                project=project, git_ref=other_refs[1]
+            )
+        )
         recipe_set = getUtility(ICharmRecipeSet)
         self.assertContentEqual(recipes[:3], recipe_set.findByContext(person))
         self.assertContentEqual(
             [recipes[0], recipes[1], recipes[3]],
-            recipe_set.findByContext(project))
+            recipe_set.findByContext(project),
+        )
         self.assertContentEqual(
-            recipes[:2], recipe_set.findByContext(repository))
+            recipes[:2], recipe_set.findByContext(repository)
+        )
         self.assertContentEqual(
-            [recipes[0]], recipe_set.findByContext(refs[0]))
+            [recipes[0]], recipe_set.findByContext(refs[0])
+        )
         self.assertRaises(
-            BadCharmRecipeSearchContext, recipe_set.findByContext,
-            self.factory.makeDistribution())
+            BadCharmRecipeSearchContext,
+            recipe_set.findByContext,
+            self.factory.makeDistribution(),
+        )
 
     def test__findStaleRecipes(self):
         # Stale; not built automatically.
@@ -1254,9 +1577,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         self.factory.makeCharmRecipe(auto_build=True, is_stale=False)
         # Stale; built automatically.
         stale_daily = self.factory.makeCharmRecipe(
-            auto_build=True, is_stale=True)
+            auto_build=True, is_stale=True
+        )
         self.assertContentEqual(
-            [stale_daily], CharmRecipeSet._findStaleRecipes())
+            [stale_daily], CharmRecipeSet._findStaleRecipes()
+        )
 
     def test__findStaleRecipes_distinct(self):
         # If a charm recipe has two build requests, it only returns one
@@ -1264,10 +1589,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
         for _ in range(2):
             build_request = self.factory.makeCharmRecipeBuildRequest(
-                recipe=recipe)
+                recipe=recipe
+            )
             removeSecurityProxy(
-                removeSecurityProxy(build_request)._job).job.date_created = (
-                    datetime.now(pytz.UTC) - timedelta(days=2))
+                removeSecurityProxy(build_request)._job
+            ).job.date_created = datetime.now(pytz.UTC) - timedelta(days=2)
         self.assertContentEqual([recipe], CharmRecipeSet._findStaleRecipes())
 
     def test_makeAutoBuilds(self):
@@ -1277,17 +1603,24 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
         logger = BufferLogger()
         [build_request] = getUtility(ICharmRecipeSet).makeAutoBuilds(
-            logger=logger)
-        self.assertThat(build_request, MatchesStructure(
-            recipe=Equals(recipe),
-            status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-            requester=Equals(recipe.owner), channels=Is(None)))
+            logger=logger
+        )
+        self.assertThat(
+            build_request,
+            MatchesStructure(
+                recipe=Equals(recipe),
+                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                requester=Equals(recipe.owner),
+                channels=Is(None),
+            ),
+        )
         expected_log_entries = [
-            "DEBUG Scheduling builds of charm recipe %s/%s/%s" % (
-                recipe.owner.name, recipe.project.name, recipe.name),
-            ]
+            "DEBUG Scheduling builds of charm recipe %s/%s/%s"
+            % (recipe.owner.name, recipe.project.name, recipe.name),
+        ]
         self.assertEqual(
-            expected_log_entries, logger.getLogBuffer().splitlines())
+            expected_log_entries, logger.getLogBuffer().splitlines()
+        )
         self.assertFalse(recipe.is_stale)
 
     def test_makeAutoBuild_skips_for_unexpected_exceptions(self):
@@ -1295,19 +1628,22 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
         logger = BufferLogger()
         recipe = removeSecurityProxy(recipe)
+
         # currently there is no expected way that `makeAutoBuilds` could fail
         # so we fake it
         def fake_requestAutoBuilds_with_side_effect(logger=None):
             raise Exception("something unexpected went wrong")
+
         recipe.requestAutoBuilds = fake_requestAutoBuilds_with_side_effect
 
         build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds(
-            logger=logger)
+            logger=logger
+        )
 
         self.assertEqual([], build_requests)
         self.assertEqual(
-            ['ERROR something unexpected went wrong'],
-            logger.getLogBuffer().splitlines()
+            ["ERROR something unexpected went wrong"],
+            logger.getLogBuffer().splitlines(),
         )
 
     def test_makeAutoBuild_skips_and_no_logger_enabled(self):
@@ -1316,8 +1652,10 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         # but we particularly test with no logger enabled.
         recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
         recipe = removeSecurityProxy(recipe)
+
         def fake_requestAutoBuilds_with_side_effect(logger=None):
             raise Exception("something unexpected went wrong")
+
         recipe.requestAutoBuilds = fake_requestAutoBuilds_with_side_effect
 
         build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds()
@@ -1329,49 +1667,67 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         # recently.
         recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
         self.factory.makeCharmRecipeBuildRequest(
-            requester=recipe.owner, recipe=recipe)
+            requester=recipe.owner, recipe=recipe
+        )
         logger = BufferLogger()
         build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds(
-            logger=logger)
+            logger=logger
+        )
         self.assertEqual([], build_requests)
         self.assertEqual([], logger.getLogBuffer().splitlines())
 
     def test_makeAutoBuilds_skips_if_requested_recently_matching_channels(
-            self):
+        self,
+    ):
         # ICharmRecipeSet.makeAutoBuilds only considers recent build
         # requests to match a recipe if they match its auto_build_channels.
         recipe1 = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
         recipe2 = self.factory.makeCharmRecipe(
-            auto_build=True, auto_build_channels={"charmcraft": "edge"},
-            is_stale=True)
+            auto_build=True,
+            auto_build_channels={"charmcraft": "edge"},
+            is_stale=True,
+        )
         # Create some build requests with mismatched channels.
         self.factory.makeCharmRecipeBuildRequest(
-            recipe=recipe1, requester=recipe1.owner,
-            channels={"charmcraft": "edge"})
+            recipe=recipe1,
+            requester=recipe1.owner,
+            channels={"charmcraft": "edge"},
+        )
         self.factory.makeCharmRecipeBuildRequest(
-            recipe=recipe2, requester=recipe2.owner,
-            channels={"charmcraft": "stable"})
+            recipe=recipe2,
+            requester=recipe2.owner,
+            channels={"charmcraft": "stable"},
+        )
 
         logger = BufferLogger()
         build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds(
-            logger=logger)
-        self.assertThat(build_requests, MatchesSetwise(
-            MatchesStructure(
-                recipe=Equals(recipe1),
-                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-                requester=Equals(recipe1.owner), channels=Is(None)),
-            MatchesStructure(
-                recipe=Equals(recipe2),
-                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-                requester=Equals(recipe2.owner),
-                channels=Equals({"charmcraft": "edge"}))))
+            logger=logger
+        )
+        self.assertThat(
+            build_requests,
+            MatchesSetwise(
+                MatchesStructure(
+                    recipe=Equals(recipe1),
+                    status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                    requester=Equals(recipe1.owner),
+                    channels=Is(None),
+                ),
+                MatchesStructure(
+                    recipe=Equals(recipe2),
+                    status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                    requester=Equals(recipe2.owner),
+                    channels=Equals({"charmcraft": "edge"}),
+                ),
+            ),
+        )
         log_entries = logger.getLogBuffer().splitlines()
         self.assertEqual(2, len(log_entries))
         for recipe in recipe1, recipe2:
             self.assertIn(
-                "DEBUG Scheduling builds of charm recipe %s/%s/%s" % (
-                    recipe.owner.name, recipe.project.name, recipe.name),
-                log_entries)
+                "DEBUG Scheduling builds of charm recipe %s/%s/%s"
+                % (recipe.owner.name, recipe.project.name, recipe.name),
+                log_entries,
+            )
             self.assertFalse(recipe.is_stale)
 
         # Mark the two recipes stale and try again.  There are now matching
@@ -1381,7 +1737,8 @@ class TestCharmRecipeSet(TestCaseWithFactory):
             IStore(recipe).flush()
         logger = BufferLogger()
         build_requests = getUtility(ICharmRecipeSet).makeAutoBuilds(
-            logger=logger)
+            logger=logger
+        )
         self.assertEqual([], build_requests)
         self.assertEqual([], logger.getLogBuffer().splitlines())
 
@@ -1396,15 +1753,21 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         recipe = self.factory.makeCharmRecipe(auto_build=True, is_stale=True)
         one_day_ago = datetime.now(pytz.UTC) - timedelta(days=1)
         build_request = self.factory.makeCharmRecipeBuildRequest(
-            recipe=recipe, requester=recipe.owner)
+            recipe=recipe, requester=recipe.owner
+        )
         removeSecurityProxy(
-            removeSecurityProxy(build_request)._job).job.date_created = (
-                one_day_ago)
+            removeSecurityProxy(build_request)._job
+        ).job.date_created = one_day_ago
         [build_request] = getUtility(ICharmRecipeSet).makeAutoBuilds()
-        self.assertThat(build_request, MatchesStructure(
-            recipe=Equals(recipe),
-            status=Equals(CharmRecipeBuildRequestStatus.PENDING),
-            requester=Equals(recipe.owner), channels=Is(None)))
+        self.assertThat(
+            build_request,
+            MatchesStructure(
+                recipe=Equals(recipe),
+                status=Equals(CharmRecipeBuildRequestStatus.PENDING),
+                requester=Equals(recipe.owner),
+                channels=Is(None),
+            ),
+        )
 
     def test_makeAutoBuilds_with_older_and_newer_build_requests(self):
         # If builds of a recipe have been requested twice, and the most recent
@@ -1414,10 +1777,11 @@ class TestCharmRecipeSet(TestCaseWithFactory):
         for timediff in timedelta(days=1), timedelta(minutes=30):
             date_created = datetime.now(pytz.UTC) - timediff
             build_request = self.factory.makeCharmRecipeBuildRequest(
-                recipe=recipe, requester=recipe.owner)
+                recipe=recipe, requester=recipe.owner
+            )
             removeSecurityProxy(
-                removeSecurityProxy(build_request)._job).job.date_created = (
-                    date_created)
+                removeSecurityProxy(build_request)._job
+            ).job.date_created = date_created
         self.assertEqual([], getUtility(ICharmRecipeSet).makeAutoBuilds())
 
     def test_detachFromGitRepository(self):
@@ -1432,21 +1796,28 @@ class TestCharmRecipeSet(TestCaseWithFactory):
                 [ref] = self.factory.makeGitRefs(repository=repository)
                 paths.append(ref.path)
                 refs.append(ref)
-                recipes.append(self.factory.makeCharmRecipe(
-                    git_ref=ref, date_created=ONE_DAY_AGO))
+                recipes.append(
+                    self.factory.makeCharmRecipe(
+                        git_ref=ref, date_created=ONE_DAY_AGO
+                    )
+                )
         getUtility(ICharmRecipeSet).detachFromGitRepository(repositories[0])
         self.assertEqual(
             [None, None, repositories[1], repositories[1]],
-            [recipe.git_repository for recipe in recipes])
+            [recipe.git_repository for recipe in recipes],
+        )
         self.assertEqual(
             [None, None, paths[2], paths[3]],
-            [recipe.git_path for recipe in recipes])
+            [recipe.git_path for recipe in recipes],
+        )
         self.assertEqual(
             [None, None, refs[2], refs[3]],
-            [recipe.git_ref for recipe in recipes])
+            [recipe.git_ref for recipe in recipes],
+        )
         for recipe in recipes[:2]:
             self.assertSqlAttributeEqualsDate(
-                recipe, "date_last_modified", UTC_NOW)
+                recipe, "date_last_modified", UTC_NOW
+            )
 
 
 class TestCharmRecipeWebservice(TestCaseWithFactory):
@@ -1455,26 +1826,39 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
 
     def setUp(self):
         super().setUp()
-        self.useFixture(FeatureFixture({
-            CHARM_RECIPE_ALLOW_CREATE: "on",
-            CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
-            CHARM_RECIPE_PRIVATE_FEATURE_FLAG: "on",
-            }))
+        self.useFixture(
+            FeatureFixture(
+                {
+                    CHARM_RECIPE_ALLOW_CREATE: "on",
+                    CHARM_RECIPE_BUILD_DISTRIBUTION: "ubuntu",
+                    CHARM_RECIPE_PRIVATE_FEATURE_FLAG: "on",
+                }
+            )
+        )
         self.pushConfig("charms", charmhub_url="http://charmhub.example/";)
         self.pushConfig(
-            "launchpad", candid_service_root="https://candid.test/";)
+            "launchpad", candid_service_root="https://candid.test/";
+        )
         self.person = self.factory.makePerson(displayname="Test Person")
         self.webservice = webservice_for_person(
-            self.person, permission=OAuthPermission.WRITE_PUBLIC)
+            self.person, permission=OAuthPermission.WRITE_PUBLIC
+        )
         self.webservice.default_api_version = "devel"
         login(ANONYMOUS)
 
     def getURL(self, obj):
         return self.webservice.getAbsoluteUrl(api_url(obj))
 
-    def makeCharmRecipe(self, owner=None, project=None, name=None,
-                        git_ref=None, private=False, webservice=None,
-                        **kwargs):
+    def makeCharmRecipe(
+        self,
+        owner=None,
+        project=None,
+        name=None,
+        git_ref=None,
+        private=False,
+        webservice=None,
+        **kwargs
+    ):
         if owner is None:
             owner = self.person
         if project is None:
@@ -1490,58 +1874,80 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
         project_url = api_url(project)
         git_ref_url = api_url(git_ref)
         logout()
-        information_type = (InformationType.PROPRIETARY if private else
-                            InformationType.PUBLIC)
+        information_type = (
+            InformationType.PROPRIETARY if private else InformationType.PUBLIC
+        )
         response = webservice.named_post(
-            "/+charm-recipes", "new", owner=owner_url, project=project_url,
-            name=name, git_ref=git_ref_url,
-            information_type=information_type.title, **kwargs)
+            "/+charm-recipes",
+            "new",
+            owner=owner_url,
+            project=project_url,
+            name=name,
+            git_ref=git_ref_url,
+            information_type=information_type.title,
+            **kwargs,
+        )
         self.assertEqual(201, response.status)
         return webservice.get(response.getHeader("Location")).jsonBody()
 
     def getCollectionLinks(self, entry, member):
         """Return a list of self_link attributes of entries in a collection."""
         collection = self.webservice.get(
-            entry["%s_collection_link" % member]).jsonBody()
+            entry["%s_collection_link" % member]
+        ).jsonBody()
         return [entry["self_link"] for entry in collection["entries"]]
 
     def test_new_git(self):
         # Charm recipe creation based on a Git branch works.
         team = self.factory.makeTeam(
             owner=self.person,
-            membership_policy=TeamMembershipPolicy.RESTRICTED)
+            membership_policy=TeamMembershipPolicy.RESTRICTED,
+        )
         project = self.factory.makeProduct(owner=team)
         [ref] = self.factory.makeGitRefs()
         recipe = self.makeCharmRecipe(
-            owner=team, project=project, name="test-charm", git_ref=ref)
+            owner=team, project=project, name="test-charm", git_ref=ref
+        )
         with person_logged_in(self.person):
-            self.assertThat(recipe, ContainsDict({
-                "registrant_link": Equals(self.getURL(self.person)),
-                "owner_link": Equals(self.getURL(team)),
-                "project_link": Equals(self.getURL(project)),
-                "name": Equals("test-charm"),
-                "git_ref_link": Equals(self.getURL(ref)),
-                "build_path": Is(None),
-                "require_virtualized": Is(True),
-                }))
+            self.assertThat(
+                recipe,
+                ContainsDict(
+                    {
+                        "registrant_link": Equals(self.getURL(self.person)),
+                        "owner_link": Equals(self.getURL(team)),
+                        "project_link": Equals(self.getURL(project)),
+                        "name": Equals("test-charm"),
+                        "git_ref_link": Equals(self.getURL(ref)),
+                        "build_path": Is(None),
+                        "require_virtualized": Is(True),
+                    }
+                ),
+            )
 
     def test_new_store_options(self):
         # The store-related options in CharmRecipe.new work.
         store_name = self.factory.getUniqueUnicode()
         recipe = self.makeCharmRecipe(
-            store_upload=True, store_name=store_name, store_channels=["edge"])
+            store_upload=True, store_name=store_name, store_channels=["edge"]
+        )
         with person_logged_in(self.person):
-            self.assertThat(recipe, ContainsDict({
-                "store_upload": Is(True),
-                "store_name": Equals(store_name),
-                "store_channels": Equals(["edge"]),
-                }))
+            self.assertThat(
+                recipe,
+                ContainsDict(
+                    {
+                        "store_upload": Is(True),
+                        "store_name": Equals(store_name),
+                        "store_channels": Equals(["edge"]),
+                    }
+                ),
+            )
 
     def test_duplicate(self):
         # An attempt to create a duplicate charm recipe fails.
         team = self.factory.makeTeam(
             owner=self.person,
-            membership_policy=TeamMembershipPolicy.RESTRICTED)
+            membership_policy=TeamMembershipPolicy.RESTRICTED,
+        )
         project = self.factory.makeProduct(owner=team)
         name = self.factory.getUniqueUnicode()
         [git_ref] = self.factory.makeGitRefs()
@@ -1549,22 +1955,34 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
         project_url = api_url(project)
         git_ref_url = api_url(git_ref)
         self.makeCharmRecipe(
-            owner=team, project=project, name=name, git_ref=git_ref)
+            owner=team, project=project, name=name, git_ref=git_ref
+        )
         response = self.webservice.named_post(
-            "/+charm-recipes", "new", owner=owner_url, project=project_url,
-            name=name, git_ref=git_ref_url)
-        self.assertThat(response, MatchesStructure.byEquality(
-            status=400,
-            body=(
-                b"There is already a charm recipe with the same project, "
-                b"owner, and name.")))
+            "/+charm-recipes",
+            "new",
+            owner=owner_url,
+            project=project_url,
+            name=name,
+            git_ref=git_ref_url,
+        )
+        self.assertThat(
+            response,
+            MatchesStructure.byEquality(
+                status=400,
+                body=(
+                    b"There is already a charm recipe with the same project, "
+                    b"owner, and name."
+                ),
+            ),
+        )
 
     def test_not_owner(self):
         # If the registrant is not the owner or a member of the owner team,
         # charm recipe creation fails.
         other_person = self.factory.makePerson(displayname="Other Person")
         other_team = self.factory.makeTeam(
-            owner=other_person, displayname="Other Team")
+            owner=other_person, displayname="Other Team"
+        )
         project = self.factory.makeProduct(owner=self.person)
         [git_ref] = self.factory.makeGitRefs()
         transaction.commit()
@@ -1574,59 +1992,97 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
         git_ref_url = api_url(git_ref)
         logout()
         response = self.webservice.named_post(
-            "/+charm-recipes", "new", owner=other_person_url,
-            project=project_url, name="test-charm", git_ref=git_ref_url)
-        self.assertThat(response, MatchesStructure.byEquality(
-            status=401,
-            body=(
-                b"Test Person cannot create charm recipes owned by "
-                b"Other Person.")))
+            "/+charm-recipes",
+            "new",
+            owner=other_person_url,
+            project=project_url,
+            name="test-charm",
+            git_ref=git_ref_url,
+        )
+        self.assertThat(
+            response,
+            MatchesStructure.byEquality(
+                status=401,
+                body=(
+                    b"Test Person cannot create charm recipes owned by "
+                    b"Other Person."
+                ),
+            ),
+        )
         response = self.webservice.named_post(
-            "/+charm-recipes", "new", owner=other_team_url,
-            project=project_url, name="test-charm", git_ref=git_ref_url)
-        self.assertThat(response, MatchesStructure.byEquality(
-            status=401,
-            body=b"Test Person is not a member of Other Team."))
+            "/+charm-recipes",
+            "new",
+            owner=other_team_url,
+            project=project_url,
+            name="test-charm",
+            git_ref=git_ref_url,
+        )
+        self.assertThat(
+            response,
+            MatchesStructure.byEquality(
+                status=401, body=b"Test Person is not a member of Other Team."
+            ),
+        )
 
     def test_cannot_set_private_components_of_public_recipe(self):
         # If a charm recipe is public, then trying to change its owner or
         # git_ref components to be private fails.
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, owner=self.person,
-            git_ref=self.factory.makeGitRefs()[0])
+            registrant=self.person,
+            owner=self.person,
+            git_ref=self.factory.makeGitRefs()[0],
+        )
         private_team = self.factory.makeTeam(
-            owner=self.person, visibility=PersonVisibility.PRIVATE)
+            owner=self.person, visibility=PersonVisibility.PRIVATE
+        )
         [private_ref] = self.factory.makeGitRefs(
-            owner=self.person,
-            information_type=InformationType.PRIVATESECURITY)
+            owner=self.person, information_type=InformationType.PRIVATESECURITY
+        )
         recipe_url = api_url(recipe)
         with person_logged_in(self.person):
             private_team_url = api_url(private_team)
             private_ref_url = api_url(private_ref)
         logout()
         private_webservice = webservice_for_person(
-            self.person, permission=OAuthPermission.WRITE_PRIVATE)
+            self.person, permission=OAuthPermission.WRITE_PRIVATE
+        )
         private_webservice.default_api_version = "devel"
         response = private_webservice.patch(
-            recipe_url, "application/json",
-            json.dumps({"owner_link": private_team_url}))
-        self.assertThat(response, MatchesStructure.byEquality(
-            status=400,
-            body=b"A public charm recipe cannot have a private owner."))
+            recipe_url,
+            "application/json",
+            json.dumps({"owner_link": private_team_url}),
+        )
+        self.assertThat(
+            response,
+            MatchesStructure.byEquality(
+                status=400,
+                body=b"A public charm recipe cannot have a private owner.",
+            ),
+        )
         response = private_webservice.patch(
-            recipe_url, "application/json",
-            json.dumps({"git_ref_link": private_ref_url}))
-        self.assertThat(response, MatchesStructure.byEquality(
-            status=400,
-            body=b"A public charm recipe cannot have a private repository."))
+            recipe_url,
+            "application/json",
+            json.dumps({"git_ref_link": private_ref_url}),
+        )
+        self.assertThat(
+            response,
+            MatchesStructure.byEquality(
+                status=400,
+                body=(
+                    b"A public charm recipe cannot have a private repository."
+                ),
+            ),
+        )
 
     def test_is_stale(self):
         # is_stale is exported and is read-only.
         recipe = self.makeCharmRecipe()
         self.assertTrue(recipe["is_stale"])
         response = self.webservice.patch(
-            recipe["self_link"], "application/json",
-            json.dumps({"is_stale": False}))
+            recipe["self_link"],
+            "application/json",
+            json.dumps({"is_stale": False}),
+        )
         self.assertEqual(400, response.status)
 
     def test_getByName(self):
@@ -1638,8 +2094,12 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
             owner_url = api_url(self.person)
             project_url = api_url(project)
         response = self.webservice.named_get(
-            "/+charm-recipes", "getByName",
-            owner=owner_url, project=project_url, name=name)
+            "/+charm-recipes",
+            "getByName",
+            owner=owner_url,
+            project=project_url,
+            name=name,
+        )
         self.assertEqual(200, response.status)
         self.assertEqual(recipe, response.jsonBody())
 
@@ -1652,65 +2112,90 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
             owner_url = api_url(self.person)
             project_url = api_url(project)
         response = self.webservice.named_get(
-            "/+charm-recipes", "getByName",
-            owner=owner_url, project=project_url, name="nonexistent")
-        self.assertThat(response, MatchesStructure.byEquality(
-            status=404,
-            body=(
-                b"No such charm recipe with this owner and project: "
-                b"'nonexistent'.")))
+            "/+charm-recipes",
+            "getByName",
+            owner=owner_url,
+            project=project_url,
+            name="nonexistent",
+        )
+        self.assertThat(
+            response,
+            MatchesStructure.byEquality(
+                status=404,
+                body=(
+                    b"No such charm recipe with this owner and project: "
+                    b"'nonexistent'."
+                ),
+            ),
+        )
 
     @responses.activate
     def assertBeginsAuthorization(self, recipe, **kwargs):
         recipe_url = api_url(recipe)
         root_macaroon = Macaroon(version=2)
         root_macaroon.add_third_party_caveat(
-            "https://candid.test/";, "", "identity")
+            "https://candid.test/";, "", "identity"
+        )
         root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
         logout()
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens";,
-            json={"macaroon": root_macaroon_raw})
+            "POST",
+            "http://charmhub.example/v1/tokens";,
+            json={"macaroon": root_macaroon_raw},
+        )
         response = self.webservice.named_post(
-            recipe_url, "beginAuthorization", **kwargs)
+            recipe_url, "beginAuthorization", **kwargs
+        )
         [call] = responses.calls
-        self.assertThat(call.request, MatchesStructure.byEquality(
-            url="http://charmhub.example/v1/tokens";, method="POST"))
+        self.assertThat(
+            call.request,
+            MatchesStructure.byEquality(
+                url="http://charmhub.example/v1/tokens";, method="POST"
+            ),
+        )
         with person_logged_in(self.person):
             expected_body = {
                 "description": (
-                    "{} for launchpad.test".format(recipe.store_name)),
+                    "{} for launchpad.test".format(recipe.store_name)
+                ),
                 "packages": [{"type": "charm", "name": recipe.store_name}],
                 "permissions": [
                     "package-manage-releases",
                     "package-manage-revisions",
                     "package-view-revisions",
-                    ],
-                }
+                ],
+            }
             self.assertEqual(
-                expected_body, json.loads(call.request.body.decode("UTF-8")))
+                expected_body, json.loads(call.request.body.decode("UTF-8"))
+            )
             self.assertEqual({"root": root_macaroon_raw}, recipe.store_secrets)
         return response, root_macaroon_raw
 
     def test_beginAuthorization(self):
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, store_upload=True,
-            store_name=self.factory.getUniqueUnicode())
+            registrant=self.person,
+            store_upload=True,
+            store_name=self.factory.getUniqueUnicode(),
+        )
         response, root_macaroon_raw = self.assertBeginsAuthorization(recipe)
         self.assertEqual(root_macaroon_raw, response.jsonBody())
 
     def test_beginAuthorization_unauthorized(self):
         # A user without edit access cannot authorize charm recipe uploads.
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, store_upload=True,
-            store_name=self.factory.getUniqueUnicode())
+            registrant=self.person,
+            store_upload=True,
+            store_name=self.factory.getUniqueUnicode(),
+        )
         recipe_url = api_url(recipe)
         other_person = self.factory.makePerson()
         other_webservice = webservice_for_person(
-            other_person, permission=OAuthPermission.WRITE_PUBLIC)
+            other_person, permission=OAuthPermission.WRITE_PUBLIC
+        )
         other_webservice.default_api_version = "devel"
         response = other_webservice.named_post(
-            recipe_url, "beginAuthorization")
+            recipe_url, "beginAuthorization"
+        )
         self.assertEqual(401, response.status)
 
     @responses.activate
@@ -1719,134 +2204,197 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
         self.pushConfig(
             "charms",
             charmhub_secrets_public_key=base64.b64encode(
-                bytes(private_key.public_key)).decode())
+                bytes(private_key.public_key)
+            ).decode(),
+        )
         root_macaroon = Macaroon(version=2)
         root_macaroon_raw = root_macaroon.serialize(JsonSerializer())
         unbound_discharge_macaroon = Macaroon(version=2)
         unbound_discharge_macaroon_raw = unbound_discharge_macaroon.serialize(
-            JsonSerializer())
+            JsonSerializer()
+        )
         discharge_macaroon_raw = root_macaroon.prepare_for_request(
-            unbound_discharge_macaroon).serialize(JsonSerializer())
+            unbound_discharge_macaroon
+        ).serialize(JsonSerializer())
         exchanged_macaroon = Macaroon(version=2)
         exchanged_macaroon_raw = exchanged_macaroon.serialize(JsonSerializer())
         responses.add(
-            "POST", "http://charmhub.example/v1/tokens/exchange";,
-            json={"macaroon": exchanged_macaroon_raw})
+            "POST",
+            "http://charmhub.example/v1/tokens/exchange";,
+            json={"macaroon": exchanged_macaroon_raw},
+        )
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, store_upload=True,
+            registrant=self.person,
+            store_upload=True,
             store_name=self.factory.getUniqueUnicode(),
-            store_secrets={"root": root_macaroon_raw})
+            store_secrets={"root": root_macaroon_raw},
+        )
         recipe_url = api_url(recipe)
         logout()
         response = self.webservice.named_post(
-            recipe_url, "completeAuthorization",
-            discharge_macaroon=json.dumps(unbound_discharge_macaroon_raw))
+            recipe_url,
+            "completeAuthorization",
+            discharge_macaroon=json.dumps(unbound_discharge_macaroon_raw),
+        )
         self.assertEqual(200, response.status)
         self.pushConfig(
             "charms",
             charmhub_secrets_private_key=base64.b64encode(
-                bytes(private_key)).decode())
+                bytes(private_key)
+            ).decode(),
+        )
         container = getUtility(IEncryptedContainer, "charmhub-secrets")
         with person_logged_in(self.person):
-            self.assertThat(recipe.store_secrets, MatchesDict({
-                "exchanged_encrypted": AfterPreprocessing(
-                    lambda data: container.decrypt(data).decode(),
-                    Equals(exchanged_macaroon_raw)),
-                }))
-        self.assertThat(responses.calls, MatchesListwise([
-            MatchesStructure(
-                request=MatchesStructure(
-                    url=Equals("http://charmhub.example/v1/tokens/exchange";),
-                    method=Equals("POST"),
-                    headers=ContainsDict({
-                        "Macaroons": AfterPreprocessing(
-                            lambda v: json.loads(
-                                base64.b64decode(v.encode()).decode()),
-                            Equals([
-                                json.loads(m) for m in (
+            self.assertThat(
+                recipe.store_secrets,
+                MatchesDict(
+                    {
+                        "exchanged_encrypted": AfterPreprocessing(
+                            lambda data: container.decrypt(data).decode(),
+                            Equals(exchanged_macaroon_raw),
+                        ),
+                    }
+                ),
+            )
+        exchange_matcher = MatchesStructure(
+            url=Equals("http://charmhub.example/v1/tokens/exchange";),
+            method=Equals("POST"),
+            headers=ContainsDict(
+                {
+                    "Macaroons": AfterPreprocessing(
+                        lambda v: json.loads(
+                            base64.b64decode(v.encode()).decode()
+                        ),
+                        Equals(
+                            [
+                                json.loads(m)
+                                for m in (
                                     root_macaroon_raw,
-                                    discharge_macaroon_raw)])),
-                        }),
-                    body=AfterPreprocessing(
-                        lambda b: json.loads(b.decode()),
-                        Equals({})))),
-            ]))
+                                    discharge_macaroon_raw,
+                                )
+                            ]
+                        ),
+                    ),
+                }
+            ),
+            body=AfterPreprocessing(
+                lambda b: json.loads(b.decode()), Equals({})
+            ),
+        )
+        self.assertThat(
+            responses.calls,
+            MatchesListwise([MatchesStructure(request=exchange_matcher)]),
+        )
 
     def test_completeAuthorization_without_beginAuthorization(self):
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, store_upload=True,
-            store_name=self.factory.getUniqueUnicode())
+            registrant=self.person,
+            store_upload=True,
+            store_name=self.factory.getUniqueUnicode(),
+        )
         recipe_url = api_url(recipe)
         logout()
         discharge_macaroon = Macaroon(version=2)
         response = self.webservice.named_post(
-            recipe_url, "completeAuthorization",
+            recipe_url,
+            "completeAuthorization",
             discharge_macaroon=json.dumps(
-                discharge_macaroon.serialize(JsonSerializer())))
-        self.assertThat(response, MatchesStructure.byEquality(
-            status=400,
-            body=(
-                b"beginAuthorization must be called before "
-                b"completeAuthorization.")))
+                discharge_macaroon.serialize(JsonSerializer())
+            ),
+        )
+        self.assertThat(
+            response,
+            MatchesStructure.byEquality(
+                status=400,
+                body=(
+                    b"beginAuthorization must be called before "
+                    b"completeAuthorization."
+                ),
+            ),
+        )
 
     def test_completeAuthorization_unauthorized(self):
         root_macaroon = Macaroon(version=2)
         discharge_macaroon = Macaroon(version=2)
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, store_upload=True,
+            registrant=self.person,
+            store_upload=True,
             store_name=self.factory.getUniqueUnicode(),
-            store_secrets={"root": root_macaroon.serialize(JsonSerializer())})
+            store_secrets={"root": root_macaroon.serialize(JsonSerializer())},
+        )
         recipe_url = api_url(recipe)
         other_person = self.factory.makePerson()
         other_webservice = webservice_for_person(
-            other_person, permission=OAuthPermission.WRITE_PUBLIC)
+            other_person, permission=OAuthPermission.WRITE_PUBLIC
+        )
         other_webservice.default_api_version = "devel"
         response = other_webservice.named_post(
-            recipe_url, "completeAuthorization",
+            recipe_url,
+            "completeAuthorization",
             discharge_macaroon=json.dumps(
-                discharge_macaroon.serialize(JsonSerializer())))
+                discharge_macaroon.serialize(JsonSerializer())
+            ),
+        )
         self.assertEqual(401, response.status)
 
     def test_completeAuthorization_malformed_discharge_macaroon(self):
         root_macaroon = Macaroon(version=2)
         recipe = self.factory.makeCharmRecipe(
-            registrant=self.person, store_upload=True,
+            registrant=self.person,
+            store_upload=True,
             store_name=self.factory.getUniqueUnicode(),
-            store_secrets={"root": root_macaroon.serialize(JsonSerializer())})
+            store_secrets={"root": root_macaroon.serialize(JsonSerializer())},
+        )
         recipe_url = api_url(recipe)
         logout()
         response = self.webservice.named_post(
-            recipe_url, "completeAuthorization",
-            discharge_macaroon="nonsense")
-        self.assertThat(response, MatchesStructure.byEquality(
-            status=400, body=b"Discharge macaroon is invalid."))
-
-    def makeBuildableDistroArchSeries(self, distroseries=None,
-                                      architecturetag=None, processor=None,
-                                      supports_virtualized=True,
-                                      supports_nonvirtualized=True, **kwargs):
+            recipe_url, "completeAuthorization", discharge_macaroon="nonsense"
+        )
+        self.assertThat(
+            response,
+            MatchesStructure.byEquality(
+                status=400, body=b"Discharge macaroon is invalid."
+            ),
+        )
+
+    def makeBuildableDistroArchSeries(
+        self,
+        distroseries=None,
+        architecturetag=None,
+        processor=None,
+        supports_virtualized=True,
+        supports_nonvirtualized=True,
+        **kwargs
+    ):
         if architecturetag is None:
             architecturetag = self.factory.getUniqueUnicode("arch")
         if processor is None:
             try:
                 processor = getUtility(IProcessorSet).getByName(
-                    architecturetag)
+                    architecturetag
+                )
             except ProcessorNotFound:
                 processor = self.factory.makeProcessor(
                     name=architecturetag,
                     supports_virtualized=supports_virtualized,
-                    supports_nonvirtualized=supports_nonvirtualized)
+                    supports_nonvirtualized=supports_nonvirtualized,
+                )
         das = self.factory.makeDistroArchSeries(
-            distroseries=distroseries, architecturetag=architecturetag,
-            processor=processor, **kwargs)
+            distroseries=distroseries,
+            architecturetag=architecturetag,
+            processor=processor,
+            **kwargs,
+        )
         # Add both a chroot and a LXD image to test that
         # getAllowedArchitectures doesn't get confused by multiple
         # PocketChroot rows for a single DistroArchSeries.
         fake_chroot = self.factory.makeLibraryFileAlias(
-            filename="fake_chroot.tar.gz", db_only=True)
+            filename="fake_chroot.tar.gz", db_only=True
+        )
         das.addOrUpdateChroot(fake_chroot)
         fake_lxd = self.factory.makeLibraryFileAlias(
-            filename="fake_lxd.tar.gz", db_only=True)
+            filename="fake_lxd.tar.gz", db_only=True
+        )
         das.addOrUpdateChroot(fake_lxd, image_type=BuildBaseImageType.LXD)
         return das
 
@@ -1856,42 +2404,58 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
         # the status of the asynchronous job.
         distroseries = self.factory.makeDistroSeries(
             distribution=getUtility(ILaunchpadCelebrities).ubuntu,
-            registrant=self.person)
+            registrant=self.person,
+        )
         processors = [
             self.factory.makeProcessor(supports_virtualized=True)
-            for _ in range(3)]
+            for _ in range(3)
+        ]
         for processor in processors:
             self.makeBuildableDistroArchSeries(
-                distroseries=distroseries, architecturetag=processor.name,
-                processor=processor, owner=self.person)
+                distroseries=distroseries,
+                architecturetag=processor.name,
+                processor=processor,
+                owner=self.person,
+            )
         [git_ref] = self.factory.makeGitRefs()
         recipe = self.makeCharmRecipe(git_ref=git_ref)
         now = get_transaction_timestamp(IStore(distroseries))
         response = self.webservice.named_post(
-            recipe["self_link"], "requestBuilds",
-            channels={"charmcraft": "edge"})
+            recipe["self_link"],
+            "requestBuilds",
+            channels={"charmcraft": "edge"},
+        )
         self.assertEqual(201, response.status)
         build_request_url = response.getHeader("Location")
         build_request = self.webservice.get(build_request_url).jsonBody()
-        self.assertThat(build_request, ContainsDict({
-            "date_requested": AfterPreprocessing(
-                iso8601.parse_date, GreaterThan(now)),
-            "date_finished": Is(None),
-            "recipe_link": Equals(recipe["self_link"]),
-            "status": Equals("Pending"),
-            "error_message": Is(None),
-            "builds_collection_link": Equals(build_request_url + "/builds"),
-            }))
+        self.assertThat(
+            build_request,
+            ContainsDict(
+                {
+                    "date_requested": AfterPreprocessing(
+                        iso8601.parse_date, GreaterThan(now)
+                    ),
+                    "date_finished": Is(None),
+                    "recipe_link": Equals(recipe["self_link"]),
+                    "status": Equals("Pending"),
+                    "error_message": Is(None),
+                    "builds_collection_link": Equals(
+                        build_request_url + "/builds"
+                    ),
+                }
+            ),
+        )
         self.assertEqual([], self.getCollectionLinks(build_request, "builds"))
         with person_logged_in(self.person):
             charmcraft_yaml = "bases:\n"
             for processor in processors:
                 charmcraft_yaml += (
-                    '  - build-on:\n'
-                    '    - name: ubuntu\n'
+                    "  - build-on:\n"
+                    "    - name: ubuntu\n"
                     '      channel: "%s"\n'
-                    '      architectures: [%s]\n' %
-                    (distroseries.version, processor.name))
+                    "      architectures: [%s]\n"
+                    % (distroseries.version, processor.name)
+                )
             self.useFixture(GitHostingFixture(blob=charmcraft_yaml))
             [job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
             with dbuser(config.ICharmRecipeRequestBuildsJobSource.dbuser):
@@ -1899,30 +2463,50 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
         date_requested = iso8601.parse_date(build_request["date_requested"])
         now = get_transaction_timestamp(IStore(distroseries))
         build_request = self.webservice.get(
-            build_request["self_link"]).jsonBody()
-        self.assertThat(build_request, ContainsDict({
-            "date_requested": AfterPreprocessing(
-                iso8601.parse_date, Equals(date_requested)),
-            "date_finished": AfterPreprocessing(
-                iso8601.parse_date,
-                MatchesAll(GreaterThan(date_requested), LessThan(now))),
-            "recipe_link": Equals(recipe["self_link"]),
-            "status": Equals("Completed"),
-            "error_message": Is(None),
-            "builds_collection_link": Equals(build_request_url + "/builds"),
-            }))
+            build_request["self_link"]
+        ).jsonBody()
+        self.assertThat(
+            build_request,
+            ContainsDict(
+                {
+                    "date_requested": AfterPreprocessing(
+                        iso8601.parse_date, Equals(date_requested)
+                    ),
+                    "date_finished": AfterPreprocessing(
+                        iso8601.parse_date,
+                        MatchesAll(GreaterThan(date_requested), LessThan(now)),
+                    ),
+                    "recipe_link": Equals(recipe["self_link"]),
+                    "status": Equals("Completed"),
+                    "error_message": Is(None),
+                    "builds_collection_link": Equals(
+                        build_request_url + "/builds"
+                    ),
+                }
+            ),
+        )
         builds = self.webservice.get(
-            build_request["builds_collection_link"]).jsonBody()["entries"]
+            build_request["builds_collection_link"]
+        ).jsonBody()["entries"]
         with person_logged_in(self.person):
-            self.assertThat(builds, MatchesSetwise(*(
-                ContainsDict({
-                    "recipe_link": Equals(recipe["self_link"]),
-                    "archive_link": Equals(
-                        self.getURL(distroseries.main_archive)),
-                    "arch_tag": Equals(processor.name),
-                    "channels": Equals({"charmcraft": "edge"}),
-                    })
-                for processor in processors)))
+            self.assertThat(
+                builds,
+                MatchesSetwise(
+                    *(
+                        ContainsDict(
+                            {
+                                "recipe_link": Equals(recipe["self_link"]),
+                                "archive_link": Equals(
+                                    self.getURL(distroseries.main_archive)
+                                ),
+                                "arch_tag": Equals(processor.name),
+                                "channels": Equals({"charmcraft": "edge"}),
+                            }
+                        )
+                        for processor in processors
+                    )
+                ),
+            )
 
     def test_requestBuilds_failure(self):
         # If the asynchronous build request job fails, this is reflected in
@@ -1931,41 +2515,61 @@ class TestCharmRecipeWebservice(TestCaseWithFactory):
         recipe = self.makeCharmRecipe(git_ref=git_ref)
         now = get_transaction_timestamp(IStore(git_ref))
         response = self.webservice.named_post(
-            recipe["self_link"], "requestBuilds")
+            recipe["self_link"], "requestBuilds"
+        )
         self.assertEqual(201, response.status)
         build_request_url = response.getHeader("Location")
         build_request = self.webservice.get(build_request_url).jsonBody()
-        self.assertThat(build_request, ContainsDict({
-            "date_requested": AfterPreprocessing(
-                iso8601.parse_date, GreaterThan(now)),
-            "date_finished": Is(None),
-            "recipe_link": Equals(recipe["self_link"]),
-            "status": Equals("Pending"),
-            "error_message": Is(None),
-            "builds_collection_link": Equals(build_request_url + "/builds"),
-            }))
+        self.assertThat(
+            build_request,
+            ContainsDict(
+                {
+                    "date_requested": AfterPreprocessing(
+                        iso8601.parse_date, GreaterThan(now)
+                    ),
+                    "date_finished": Is(None),
+                    "recipe_link": Equals(recipe["self_link"]),
+                    "status": Equals("Pending"),
+                    "error_message": Is(None),
+                    "builds_collection_link": Equals(
+                        build_request_url + "/builds"
+                    ),
+                }
+            ),
+        )
         self.assertEqual([], self.getCollectionLinks(build_request, "builds"))
         with person_logged_in(self.person):
             self.useFixture(GitHostingFixture()).getBlob.failure = Exception(
-                "Something went wrong")
+                "Something went wrong"
+            )
             [job] = getUtility(ICharmRecipeRequestBuildsJobSource).iterReady()
             with dbuser(config.ICharmRecipeRequestBuildsJobSource.dbuser):
                 JobRunner([job]).runAll()
         date_requested = iso8601.parse_date(build_request["date_requested"])
         now = get_transaction_timestamp(IStore(git_ref))
         build_request = self.webservice.get(
-            build_request["self_link"]).jsonBody()
-        self.assertThat(build_request, ContainsDict({
-            "date_requested": AfterPreprocessing(
-                iso8601.parse_date, Equals(date_requested)),
-            "date_finished": AfterPreprocessing(
-                iso8601.parse_date,
-                MatchesAll(GreaterThan(date_requested), LessThan(now))),
-            "recipe_link": Equals(recipe["self_link"]),
-            "status": Equals("Failed"),
-            "error_message": Equals("Something went wrong"),
-            "builds_collection_link": Equals(build_request_url + "/builds"),
-            }))
+            build_request["self_link"]
+        ).jsonBody()
+        self.assertThat(
+            build_request,
+            ContainsDict(
+                {
+                    "date_requested": AfterPreprocessing(
+                        iso8601.parse_date, Equals(date_requested)
+                    ),
+                    "date_finished": AfterPreprocessing(
+                        iso8601.parse_date,
+                        MatchesAll(GreaterThan(date_requested), LessThan(now)),
+                    ),
+                    "recipe_link": Equals(recipe["self_link"]),
+                    "status": Equals("Failed"),
+                    "error_message": Equals("Something went wrong"),
+                    "builds_collection_link": Equals(
+                        build_request_url + "/builds"
+                    ),
+                }
+