← Back to team overview

launchpad-reviewers team mailing list archive

[Merge] ~cjwatson/launchpad:artifactory-pypi into launchpad:master

 

Colin Watson has proposed merging ~cjwatson/launchpad:artifactory-pypi into launchpad:master with ~cjwatson/launchpad:diskpool-add-source-version as a prerequisite.

Commit message:
Handle path changes to publish Python packages via Artifactory

Requested reviews:
  Launchpad code reviewers (launchpad-reviewers)

For more details, see:
https://code.launchpad.net/~cjwatson/launchpad/+git/launchpad/+merge/423563

Unlike .debs, Python packages (sdists and wheels) are published in <name>/<version>/ subdirectories.
-- 
Your team Launchpad code reviewers is requested to review the proposed merge of ~cjwatson/launchpad:artifactory-pypi into launchpad:master.
diff --git a/lib/lp/archivepublisher/artifactory.py b/lib/lp/archivepublisher/artifactory.py
index 53b43a6..5ce6460 100644
--- a/lib/lp/archivepublisher/artifactory.py
+++ b/lib/lp/archivepublisher/artifactory.py
@@ -41,6 +41,21 @@ from lp.soyuz.interfaces.publishing import (
     )
 
 
+def _path_for(archive: IArchive, rootpath: ArtifactoryPath, source_name: str,
+              source_version: str, filename: Optional[str] = None) -> Path:
+    repository_format = archive.repository_format
+    if repository_format == ArchiveRepositoryFormat.DEBIAN:
+        path = rootpath / poolify(source_name)
+    elif repository_format == ArchiveRepositoryFormat.PYTHON:
+        path = rootpath / source_name / source_version
+    else:
+        raise AssertionError(
+            "Unsupported repository format: %r" % repository_format)
+    if filename:
+        path = path / filename
+    return path
+
+
 class ArtifactoryPoolEntry:
 
     def __init__(self, archive: IArchive, rootpath: ArtifactoryPath,
@@ -63,7 +78,9 @@ class ArtifactoryPoolEntry:
         # the pool structure, and doing so would introduce significant
         # complications in terms of having to keep track of components just
         # in order to update an artifact's properties.
-        return self.rootpath / poolify(self.source_name) / self.filename
+        return _path_for(
+            self.archive, self.rootpath, self.source_name, self.source_version,
+            self.filename)
 
     def makeReleaseID(self, pub_file: IPackageReleaseFile) -> str:
         """
@@ -156,8 +173,7 @@ class ArtifactoryPoolEntry:
             else:
                 properties["launchpad.channel"] = sorted({
                     "%s:%s" % (
-                        pub.distroseries.getSuite(pub.pocket),
-                        pub.channel_string)
+                        pub.distroseries.getSuite(pub.pocket), pub.channel)
                     for pub in publications})
         return properties
 
@@ -285,10 +301,8 @@ class ArtifactoryPool:
         # the pool structure, and doing so would introduce significant
         # complications in terms of having to keep track of components just
         # in order to update an artifact's properties.
-        path = self.rootpath / poolify(source_name)
-        if file:
-            path = path / file
-        return path
+        return _path_for(
+            self.archive, self.rootpath, source_name, source_version, file)
 
     def addFile(self, component: str, source_name: str, source_version: str,
                 filename: str, pub_file: IPackageReleaseFile):
diff --git a/lib/lp/archivepublisher/config.py b/lib/lp/archivepublisher/config.py
index 55603dc..06ae2a7 100644
--- a/lib/lp/archivepublisher/config.py
+++ b/lib/lp/archivepublisher/config.py
@@ -20,6 +20,7 @@ from lp.soyuz.enums import (
     archive_suffixes,
     ArchivePublishingMethod,
     ArchivePurpose,
+    ArchiveRepositoryFormat,
     )
 
 
@@ -111,8 +112,12 @@ def getPubConfig(archive):
         pubconf.signingroot = None
         pubconf.signingautokey = False
 
-    pubconf.poolroot = os.path.join(pubconf.archiveroot, 'pool')
-    pubconf.distsroot = os.path.join(pubconf.archiveroot, 'dists')
+    if archive.repository_format == ArchiveRepositoryFormat.DEBIAN:
+        pubconf.poolroot = os.path.join(pubconf.archiveroot, 'pool')
+        pubconf.distsroot = os.path.join(pubconf.archiveroot, 'dists')
+    else:
+        pubconf.poolroot = pubconf.archiveroot
+        pubconf.distsroot = None
 
     # META_DATA custom uploads are stored in a separate directory
     # outside the archive root so Ubuntu Software Center can get some
diff --git a/lib/lp/archivepublisher/tests/test_artifactory.py b/lib/lp/archivepublisher/tests/test_artifactory.py
index f611ac9..6954a2c 100644
--- a/lib/lp/archivepublisher/tests/test_artifactory.py
+++ b/lib/lp/archivepublisher/tests/test_artifactory.py
@@ -5,6 +5,7 @@
 
 from pathlib import PurePath
 
+from artifactory import ArtifactoryPath
 import transaction
 from zope.component import getUtility
 
@@ -18,12 +19,16 @@ from lp.archivepublisher.tests.test_pool import (
     PoolTestingFile,
     )
 from lp.registry.interfaces.pocket import PackagePublishingPocket
-from lp.registry.interfaces.sourcepackage import SourcePackageFileType
+from lp.registry.interfaces.sourcepackage import (
+    SourcePackageFileType,
+    SourcePackageType,
+    )
 from lp.services.log.logger import BufferLogger
 from lp.soyuz.enums import (
     ArchivePurpose,
     ArchiveRepositoryFormat,
     BinaryPackageFileType,
+    BinaryPackageFormat,
     )
 from lp.soyuz.interfaces.publishing import (
     IPublishingSet,
@@ -77,16 +82,53 @@ class TestArtifactoryPool(TestCase):
         self.repository_name = "repository"
         self.artifactory = self.useFixture(
             FakeArtifactoryFixture(self.base_url, self.repository_name))
-        root_url = "%s/%s/pool" % (self.base_url, self.repository_name)
-        self.pool = ArtifactoryPool(FakeArchive(), root_url, BufferLogger())
+
+    def makePool(self, repository_format=ArchiveRepositoryFormat.DEBIAN):
+        # Matches behaviour of lp.archivepublisher.config.getPubConfig.
+        root_url = "%s/%s" % (self.base_url, self.repository_name)
+        if repository_format == ArchiveRepositoryFormat.DEBIAN:
+            root_url += "/pool"
+        return ArtifactoryPool(
+            FakeArchive(repository_format), root_url, BufferLogger())
+
+    def test_pathFor_debian_without_file(self):
+        pool = self.makePool()
+        self.assertEqual(
+            ArtifactoryPath(
+                "https://foo.example.com/artifactory/repository/pool/f/foo";),
+            pool.pathFor(None, "foo", "1.0"))
+
+    def test_pathFor_debian_with_file(self):
+        pool = self.makePool()
+        self.assertEqual(
+            ArtifactoryPath(
+                "https://foo.example.com/artifactory/repository/pool/f/foo/";
+                "foo-1.0.deb"),
+            pool.pathFor(None, "foo", "1.0", "foo-1.0.deb"))
+
+    def test_pathFor_python_without_file(self):
+        pool = self.makePool(ArchiveRepositoryFormat.PYTHON)
+        self.assertEqual(
+            ArtifactoryPath(
+                "https://foo.example.com/artifactory/repository/foo/1.0";),
+            pool.pathFor(None, "foo", "1.0"))
+
+    def test_pathFor_python_with_file(self):
+        pool = self.makePool(ArchiveRepositoryFormat.PYTHON)
+        self.assertEqual(
+            ArtifactoryPath(
+                "https://foo.example.com/artifactory/repository/foo/1.0/";
+                "foo-1.0.whl"),
+            pool.pathFor(None, "foo", "1.0", "foo-1.0.whl"))
 
     def test_addFile(self):
+        pool = self.makePool()
         foo = ArtifactoryPoolTestingFile(
-            self.pool, "foo", "1.0", "foo-1.0.deb",
+            pool, "foo", "1.0", "foo-1.0.deb",
             release_type=FakeReleaseType.BINARY, release_id=1)
         self.assertFalse(foo.checkIsFile())
         result = foo.addToPool()
-        self.assertEqual(self.pool.results.FILE_ADDED, result)
+        self.assertEqual(pool.results.FILE_ADDED, result)
         self.assertTrue(foo.checkIsFile())
         self.assertEqual(
             {
@@ -97,18 +139,20 @@ class TestArtifactoryPool(TestCase):
             foo.getProperties())
 
     def test_addFile_exists_identical(self):
+        pool = self.makePool()
         foo = ArtifactoryPoolTestingFile(
-            self.pool, "foo", "1.0", "foo-1.0.deb",
+            pool, "foo", "1.0", "foo-1.0.deb",
             release_type=FakeReleaseType.BINARY, release_id=1)
         foo.addToPool()
         self.assertTrue(foo.checkIsFile())
         result = foo.addToPool()
-        self.assertEqual(self.pool.results.NONE, result)
+        self.assertEqual(pool.results.NONE, result)
         self.assertTrue(foo.checkIsFile())
 
     def test_addFile_exists_overwrite(self):
+        pool = self.makePool()
         foo = ArtifactoryPoolTestingFile(
-            self.pool, "foo", "1.0", "foo-1.0.deb",
+            pool, "foo", "1.0", "foo-1.0.deb",
             release_type=FakeReleaseType.BINARY, release_id=1)
         foo.addToPool()
         self.assertTrue(foo.checkIsFile())
@@ -116,8 +160,8 @@ class TestArtifactoryPool(TestCase):
         self.assertRaises(PoolFileOverwriteError, foo.addToPool)
 
     def test_removeFile(self):
-        foo = ArtifactoryPoolTestingFile(
-            self.pool, "foo", "1.0", "foo-1.0.deb")
+        pool = self.makePool()
+        foo = ArtifactoryPoolTestingFile(pool, "foo", "1.0", "foo-1.0.deb")
         foo.addToPool()
         self.assertTrue(foo.checkIsFile())
         size = foo.removeFromPool()
@@ -125,6 +169,7 @@ class TestArtifactoryPool(TestCase):
         self.assertEqual(3, size)
 
     def test_getArtifactPatterns_debian(self):
+        pool = self.makePool()
         self.assertEqual(
             [
                 "*.ddeb",
@@ -134,12 +179,13 @@ class TestArtifactoryPool(TestCase):
                 "*.tar.*",
                 "*.udeb",
                 ],
-            self.pool.getArtifactPatterns(ArchiveRepositoryFormat.DEBIAN))
+            pool.getArtifactPatterns(ArchiveRepositoryFormat.DEBIAN))
 
     def test_getArtifactPatterns_python(self):
+        pool = self.makePool()
         self.assertEqual(
             ["*.whl"],
-            self.pool.getArtifactPatterns(ArchiveRepositoryFormat.PYTHON))
+            pool.getArtifactPatterns(ArchiveRepositoryFormat.PYTHON))
 
     def test_getAllArtifacts(self):
         # getAllArtifacts mostly relies on constructing a correct AQL query,
@@ -147,14 +193,15 @@ class TestArtifactoryPool(TestCase):
         # instance, although `FakeArtifactoryFixture` tries to do something
         # with it.  This test mainly ensures that we transform the response
         # correctly.
+        pool = self.makePool()
         ArtifactoryPoolTestingFile(
-            self.pool, "foo", "1.0", "foo-1.0.deb",
+            pool, "foo", "1.0", "foo-1.0.deb",
             release_type=FakeReleaseType.BINARY, release_id=1).addToPool()
         ArtifactoryPoolTestingFile(
-            self.pool, "foo", "1.1", "foo-1.1.deb",
+            pool, "foo", "1.1", "foo-1.1.deb",
             release_type=FakeReleaseType.BINARY, release_id=2).addToPool()
         ArtifactoryPoolTestingFile(
-            self.pool, "bar", "1.0", "bar-1.0.whl",
+            pool, "bar", "1.0", "bar-1.0.whl",
             release_type=FakeReleaseType.BINARY, release_id=3).addToPool()
         self.assertEqual(
             {
@@ -169,7 +216,7 @@ class TestArtifactoryPool(TestCase):
                     "launchpad.source-version": ["1.1"],
                     },
                 },
-            self.pool.getAllArtifacts(
+            pool.getAllArtifacts(
                 self.repository_name, ArchiveRepositoryFormat.DEBIAN))
         self.assertEqual(
             {
@@ -179,7 +226,7 @@ class TestArtifactoryPool(TestCase):
                     "launchpad.source-version": ["1.0"],
                     },
                 },
-            self.pool.getAllArtifacts(
+            pool.getAllArtifacts(
                 self.repository_name, ArchiveRepositoryFormat.PYTHON))
 
 
@@ -193,17 +240,24 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
         self.repository_name = "repository"
         self.artifactory = self.useFixture(
             FakeArtifactoryFixture(self.base_url, self.repository_name))
-        root_url = "%s/%s/pool" % (self.base_url, self.repository_name)
-        self.archive = self.factory.makeArchive(purpose=ArchivePurpose.PPA)
-        self.pool = ArtifactoryPool(self.archive, root_url, BufferLogger())
+
+    def makePool(self, repository_format=ArchiveRepositoryFormat.DEBIAN):
+        # Matches behaviour of lp.archivepublisher.config.getPubConfig.
+        root_url = "%s/%s" % (self.base_url, self.repository_name)
+        if repository_format == ArchiveRepositoryFormat.DEBIAN:
+            root_url += "/pool"
+        archive = self.factory.makeArchive(
+            purpose=ArchivePurpose.PPA, repository_format=repository_format)
+        return ArtifactoryPool(archive, root_url, BufferLogger())
 
     def test_updateProperties_debian_source(self):
+        pool = self.makePool()
         dses = [
             self.factory.makeDistroSeries(
-                distribution=self.archive.distribution)
+                distribution=pool.archive.distribution)
             for _ in range(2)]
         spph = self.factory.makeSourcePackagePublishingHistory(
-            archive=self.archive, distroseries=dses[0],
+            archive=pool.archive, distroseries=dses[0],
             pocket=PackagePublishingPocket.RELEASE, component="main",
             sourcepackagename="foo", version="1.0")
         spr = spph.sourcepackagerelease
@@ -214,11 +268,11 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
             filetype=SourcePackageFileType.DSC)
         spphs = [spph]
         spphs.append(spph.copyTo(
-            dses[1], PackagePublishingPocket.RELEASE, self.archive))
+            dses[1], PackagePublishingPocket.RELEASE, pool.archive))
         transaction.commit()
-        self.pool.addFile(
+        pool.addFile(
             None, spr.name, spr.version, sprf.libraryfile.filename, sprf)
-        path = self.pool.rootpath / "f" / "foo" / "foo_1.0.dsc"
+        path = pool.rootpath / "f" / "foo" / "foo_1.0.dsc"
         self.assertTrue(path.exists())
         self.assertFalse(path.is_symlink())
         self.assertEqual(
@@ -228,7 +282,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
                 "launchpad.source-version": ["1.0"],
                 },
             path.properties)
-        self.pool.updateProperties(
+        pool.updateProperties(
             spr.name, spr.version, sprf.libraryfile.filename, spphs)
         self.assertEqual(
             {
@@ -241,9 +295,10 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
             path.properties)
 
     def test_updateProperties_debian_binary_multiple_series(self):
+        pool = self.makePool()
         dses = [
             self.factory.makeDistroSeries(
-                distribution=self.archive.distribution)
+                distribution=pool.archive.distribution)
             for _ in range(2)]
         processor = self.factory.makeProcessor()
         dases = [
@@ -251,9 +306,9 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
                 distroseries=ds, architecturetag=processor.name)
             for ds in dses]
         spr = self.factory.makeSourcePackageRelease(
-            archive=self.archive, sourcepackagename="foo", version="1.0")
+            archive=pool.archive, sourcepackagename="foo", version="1.0")
         bpph = self.factory.makeBinaryPackagePublishingHistory(
-            archive=self.archive, distroarchseries=dases[0],
+            archive=pool.archive, distroarchseries=dases[0],
             pocket=PackagePublishingPocket.RELEASE, component="main",
             source_package_release=spr, binarypackagename="foo",
             architecturespecific=True)
@@ -265,14 +320,13 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
             filetype=BinaryPackageFileType.DEB)
         bpphs = [bpph]
         bpphs.append(bpph.copyTo(
-            dses[1], PackagePublishingPocket.RELEASE, self.archive)[0])
+            dses[1], PackagePublishingPocket.RELEASE, pool.archive)[0])
         transaction.commit()
-        self.pool.addFile(
+        pool.addFile(
             None, bpr.sourcepackagename, bpr.sourcepackageversion,
             bpf.libraryfile.filename, bpf)
         path = (
-            self.pool.rootpath / "f" / "foo" /
-            ("foo_1.0_%s.deb" % processor.name))
+            pool.rootpath / "f" / "foo" / ("foo_1.0_%s.deb" % processor.name))
         self.assertTrue(path.exists())
         self.assertFalse(path.is_symlink())
         self.assertEqual(
@@ -282,7 +336,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
                 "launchpad.source-version": ["1.0"],
                 },
             path.properties)
-        self.pool.updateProperties(
+        pool.updateProperties(
             bpr.sourcepackagename, bpr.sourcepackageversion,
             bpf.libraryfile.filename, bpphs)
         self.assertEqual(
@@ -297,15 +351,16 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
             path.properties)
 
     def test_updateProperties_debian_binary_multiple_architectures(self):
+        pool = self.makePool()
         ds = self.factory.makeDistroSeries(
-            distribution=self.archive.distribution)
+            distribution=pool.archive.distribution)
         dases = [
             self.factory.makeDistroArchSeries(distroseries=ds)
             for _ in range(2)]
         spr = self.factory.makeSourcePackageRelease(
-            archive=self.archive, sourcepackagename="foo", version="1.0")
+            archive=pool.archive, sourcepackagename="foo", version="1.0")
         bpb = self.factory.makeBinaryPackageBuild(
-            archive=self.archive, source_package_release=spr,
+            archive=pool.archive, source_package_release=spr,
             distroarchseries=dases[0], pocket=PackagePublishingPocket.RELEASE)
         bpr = self.factory.makeBinaryPackageRelease(
             binarypackagename="foo", build=bpb, component="main",
@@ -316,13 +371,13 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
                 filename="foo_1.0_all.deb"),
             filetype=BinaryPackageFileType.DEB)
         bpphs = getUtility(IPublishingSet).publishBinaries(
-            self.archive, ds, PackagePublishingPocket.RELEASE,
+            pool.archive, ds, PackagePublishingPocket.RELEASE,
             {bpr: (bpr.component, bpr.section, bpr.priority, None)})
         transaction.commit()
-        self.pool.addFile(
+        pool.addFile(
             None, bpr.sourcepackagename, bpr.sourcepackageversion,
             bpf.libraryfile.filename, bpf)
-        path = self.pool.rootpath / "f" / "foo" / "foo_1.0_all.deb"
+        path = pool.rootpath / "f" / "foo" / "foo_1.0_all.deb"
         self.assertTrue(path.exists())
         self.assertFalse(path.is_symlink())
         self.assertEqual(
@@ -332,7 +387,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
                 "launchpad.source-version": ["1.0"],
                 },
             path.properties)
-        self.pool.updateProperties(
+        pool.updateProperties(
             bpr.sourcepackagename, bpr.sourcepackageversion,
             bpf.libraryfile.filename, bpphs)
         self.assertEqual(
@@ -347,16 +402,120 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
                 },
             path.properties)
 
+    def test_updateProperties_python_sdist(self):
+        pool = self.makePool(ArchiveRepositoryFormat.PYTHON)
+        dses = [
+            self.factory.makeDistroSeries(
+                distribution=pool.archive.distribution)
+            for _ in range(2)]
+        spph = self.factory.makeSourcePackagePublishingHistory(
+            archive=pool.archive, distroseries=dses[0],
+            pocket=PackagePublishingPocket.RELEASE, component="main",
+            sourcepackagename="foo", version="1.0", channel="edge",
+            format=SourcePackageType.SDIST)
+        spr = spph.sourcepackagerelease
+        sprf = self.factory.makeSourcePackageReleaseFile(
+            sourcepackagerelease=spr,
+            library_file=self.factory.makeLibraryFileAlias(
+                filename="foo-1.0.tar.gz"),
+            filetype=SourcePackageFileType.SDIST)
+        spphs = [spph]
+        spphs.append(spph.copyTo(
+            dses[1], PackagePublishingPocket.RELEASE, pool.archive))
+        transaction.commit()
+        pool.addFile(
+            None, spr.name, spr.version, sprf.libraryfile.filename, sprf)
+        path = pool.rootpath / "foo" / "1.0" / "foo-1.0.tar.gz"
+        self.assertTrue(path.exists())
+        self.assertFalse(path.is_symlink())
+        self.assertEqual(
+            {
+                "launchpad.release-id": ["source:%d" % spr.id],
+                "launchpad.source-name": ["foo"],
+                "launchpad.source-version": ["1.0"],
+                },
+            path.properties)
+        pool.updateProperties(
+            spr.name, spr.version, sprf.libraryfile.filename, spphs)
+        self.assertEqual(
+            {
+                "launchpad.release-id": ["source:%d" % spr.id],
+                "launchpad.source-name": ["foo"],
+                "launchpad.source-version": ["1.0"],
+                "launchpad.channel": list(
+                    sorted("%s:edge" % ds.name for ds in dses)),
+                },
+            path.properties)
+
+    def test_updateProperties_python_wheel(self):
+        pool = self.makePool(ArchiveRepositoryFormat.PYTHON)
+        dses = [
+            self.factory.makeDistroSeries(
+                distribution=pool.archive.distribution)
+            for _ in range(2)]
+        processor = self.factory.makeProcessor()
+        dases = [
+            self.factory.makeDistroArchSeries(
+                distroseries=ds, architecturetag=processor.name)
+            for ds in dses]
+        spr = self.factory.makeSourcePackageRelease(
+            archive=pool.archive, sourcepackagename="foo", version="1.0",
+            format=SourcePackageType.SDIST)
+        bpph = self.factory.makeBinaryPackagePublishingHistory(
+            archive=pool.archive, distroarchseries=dases[0],
+            pocket=PackagePublishingPocket.RELEASE, component="main",
+            source_package_release=spr, binarypackagename="foo",
+            binpackageformat=BinaryPackageFormat.WHL,
+            architecturespecific=False, channel="edge")
+        bpr = bpph.binarypackagerelease
+        bpf = self.factory.makeBinaryPackageFile(
+            binarypackagerelease=bpr,
+            library_file=self.factory.makeLibraryFileAlias(
+                filename="foo-1.0-py3-none-any.whl"),
+            filetype=BinaryPackageFileType.WHL)
+        bpphs = [bpph]
+        bpphs.append(
+            getUtility(IPublishingSet).copyBinaries(
+                pool.archive, dses[1], PackagePublishingPocket.RELEASE, [bpph],
+                channel="edge")[0])
+        transaction.commit()
+        pool.addFile(
+            None, bpr.sourcepackagename, bpr.sourcepackageversion,
+            bpf.libraryfile.filename, bpf)
+        path = pool.rootpath / "foo" / "1.0" / "foo-1.0-py3-none-any.whl"
+        self.assertTrue(path.exists())
+        self.assertFalse(path.is_symlink())
+        self.assertEqual(
+            {
+                "launchpad.release-id": ["binary:%d" % bpr.id],
+                "launchpad.source-name": ["foo"],
+                "launchpad.source-version": ["1.0"],
+                },
+            path.properties)
+        pool.updateProperties(
+            bpr.sourcepackagename, bpr.sourcepackageversion,
+            bpf.libraryfile.filename, bpphs)
+        self.assertEqual(
+            {
+                "launchpad.release-id": ["binary:%d" % bpr.id],
+                "launchpad.source-name": ["foo"],
+                "launchpad.source-version": ["1.0"],
+                "launchpad.channel": list(
+                    sorted("%s:edge" % ds.name for ds in dses)),
+                },
+            path.properties)
+
     def test_updateProperties_preserves_externally_set_properties(self):
         # Artifactory sets some properties by itself as part of scanning
         # packages.  We leave those untouched.
+        pool = self.makePool()
         ds = self.factory.makeDistroSeries(
-            distribution=self.archive.distribution)
+            distribution=pool.archive.distribution)
         das = self.factory.makeDistroArchSeries(distroseries=ds)
         spr = self.factory.makeSourcePackageRelease(
-            archive=self.archive, sourcepackagename="foo", version="1.0")
+            archive=pool.archive, sourcepackagename="foo", version="1.0")
         bpb = self.factory.makeBinaryPackageBuild(
-            archive=self.archive, source_package_release=spr,
+            archive=pool.archive, source_package_release=spr,
             distroarchseries=das, pocket=PackagePublishingPocket.RELEASE)
         bpr = self.factory.makeBinaryPackageRelease(
             binarypackagename="foo", build=bpb, component="main",
@@ -367,13 +526,13 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
                 filename="foo_1.0_all.deb"),
             filetype=BinaryPackageFileType.DEB)
         bpphs = getUtility(IPublishingSet).publishBinaries(
-            self.archive, ds, PackagePublishingPocket.RELEASE,
+            pool.archive, ds, PackagePublishingPocket.RELEASE,
             {bpr: (bpr.component, bpr.section, bpr.priority, None)})
         transaction.commit()
-        self.pool.addFile(
+        pool.addFile(
             None, bpr.sourcepackagename, bpr.sourcepackageversion,
             bpf.libraryfile.filename, bpf)
-        path = self.pool.rootpath / "f" / "foo" / "foo_1.0_all.deb"
+        path = pool.rootpath / "f" / "foo" / "foo_1.0_all.deb"
         path.set_properties({"deb.version": ["1.0"]}, recursive=False)
         self.assertEqual(
             {
@@ -383,7 +542,7 @@ class TestArtifactoryPoolFromLibrarian(TestCaseWithFactory):
                 "deb.version": ["1.0"],
                 },
             path.properties)
-        self.pool.updateProperties(
+        pool.updateProperties(
             bpr.sourcepackagename, bpr.sourcepackageversion,
             bpf.libraryfile.filename, bpphs)
         self.assertEqual(
diff --git a/lib/lp/archivepublisher/tests/test_config.py b/lib/lp/archivepublisher/tests/test_config.py
index 915b4e7..75b1431 100644
--- a/lib/lp/archivepublisher/tests/test_config.py
+++ b/lib/lp/archivepublisher/tests/test_config.py
@@ -16,7 +16,11 @@ from lp.archivepublisher.config import getPubConfig
 from lp.registry.interfaces.distribution import IDistributionSet
 from lp.services.config import config
 from lp.services.log.logger import BufferLogger
-from lp.soyuz.enums import ArchivePurpose
+from lp.soyuz.enums import (
+    ArchivePublishingMethod,
+    ArchivePurpose,
+    ArchiveRepositoryFormat,
+    )
 from lp.soyuz.interfaces.archive import IArchiveSet
 from lp.testing import TestCaseWithFactory
 from lp.testing.layers import ZopelessDatabaseLayer
@@ -235,3 +239,35 @@ class TestGetPubConfigPPACompatUefi(TestCaseWithFactory):
         signingroot = "/var/tmp/ppa-signing-keys.test/uefi/%s/%s" % (
             self.ppa.owner.name, self.ppa.name)
         self.assertEqual(signingroot, self.ppa_config.signingroot)
+
+
+class TestGetPubConfigPPARepositoryFormatPython(TestCaseWithFactory):
+
+    layer = ZopelessDatabaseLayer
+
+    def setUp(self):
+        super().setUp()
+        self.base_url = "https://foo.example.com/artifactory";
+        self.pushConfig("artifactory", base_url=self.base_url)
+        self.ppa = self.factory.makeArchive(
+            purpose=ArchivePurpose.PPA,
+            publishing_method=ArchivePublishingMethod.ARTIFACTORY,
+            repository_format=ArchiveRepositoryFormat.PYTHON)
+        self.ppa_config = getPubConfig(self.ppa)
+
+    def test_config(self):
+        # Python-format archives published via Artifactory use paths under
+        # the Artifactory base URL, and have various features disabled that
+        # only make sense for locally-published Debian-format archives.
+        self.assertIsNone(self.ppa_config.distroroot)
+        archiveroot = "%s/%s" % (self.base_url, self.ppa.name)
+        self.assertEqual(archiveroot, self.ppa_config.archiveroot)
+        self.assertEqual(archiveroot, self.ppa_config.poolroot)
+        self.assertIsNone(self.ppa_config.distsroot)
+        self.assertIsNone(self.ppa_config.overrideroot)
+        self.assertIsNone(self.ppa_config.cacheroot)
+        self.assertIsNone(self.ppa_config.miscroot)
+        self.assertEqual(
+            "/var/tmp/archive/%s-temp" % self.ppa.distribution.name,
+            self.ppa_config.temproot)
+        self.assertIsNone(self.ppa_config.metaroot)
diff --git a/lib/lp/registry/interfaces/sourcepackage.py b/lib/lp/registry/interfaces/sourcepackage.py
index eddd622..21d02ca 100644
--- a/lib/lp/registry/interfaces/sourcepackage.py
+++ b/lib/lp/registry/interfaces/sourcepackage.py
@@ -418,6 +418,12 @@ class SourcePackageFileType(DBEnumeratedType):
         This file is a detached signature for an Ubuntu component "orig"
         file.""")
 
+    SDIST = DBItem(11, """
+        Python Source Distribution
+
+        This file is a Python source distribution ("sdist").
+        """)
+
 
 class SourcePackageType(DBEnumeratedType):
     """Source Package Format
@@ -447,6 +453,12 @@ class SourcePackageType(DBEnumeratedType):
         This is the source package format used by Gentoo.
         """)
 
+    SDIST = DBItem(4, """
+        The Python Format
+
+        This is the source package format used by Python packages.
+        """)
+
 
 class SourcePackageUrgency(DBEnumeratedType):
     """Source Package Urgency