github.com/anchore/syft@v1.38.2/syft/pkg/cataloger/python/cataloger_test.go (about) 1 package python 2 3 import ( 4 "context" 5 "fmt" 6 "os" 7 "path" 8 "testing" 9 10 "github.com/stretchr/testify/require" 11 12 "github.com/anchore/syft/syft/file" 13 "github.com/anchore/syft/syft/pkg" 14 "github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest" 15 ) 16 17 func Test_PackageCataloger(t *testing.T) { 18 ctx := context.TODO() 19 tests := []struct { 20 name string 21 fixture string 22 expectedPackages []pkg.Package 23 }{ 24 { 25 name: "egg-file-no-version", 26 fixture: "test-fixtures/site-packages/no-version", 27 expectedPackages: []pkg.Package{ 28 { 29 Name: "no-version", 30 Locations: file.NewLocationSet(file.NewLocation("no-version-py3.8.egg-info")), 31 PURL: "pkg:pypi/no-version", 32 Type: pkg.PythonPkg, 33 Language: pkg.Python, 34 FoundBy: "python-installed-package-cataloger", 35 Metadata: pkg.PythonPackage{ 36 Name: "no-version", 37 SitePackagesRootPath: ".", // requires scanning the grandparent directory to get a valid path 38 }, 39 }, 40 }, 41 }, 42 { 43 name: "dist-info+egg-info site-packages directory", 44 fixture: "test-fixtures/site-packages/nested", 45 expectedPackages: []pkg.Package{ 46 { 47 Name: "pygments", 48 Version: "2.6.1", 49 PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 50 Type: pkg.PythonPkg, 51 Language: pkg.Python, 52 Locations: file.NewLocationSet( 53 file.NewLocation("dist-name/dist-info/METADATA"), 54 file.NewLocation("dist-name/dist-info/RECORD"), 55 file.NewLocation("dist-name/dist-info/direct_url.json"), 56 file.NewLocation("dist-name/dist-info/top_level.txt"), 57 ), 58 Licenses: pkg.NewLicenseSet( 59 // here we only used the license that was declared in the METADATA file, we did not go searching for other licenses 60 // this is the better source of truth when there is no explicit LicenseFile given 61 pkg.NewLicenseFromLocationsWithContext(ctx, "BSD License", file.NewLocation("dist-name/dist-info/METADATA")), 62 ), 63 FoundBy: "python-installed-package-cataloger", 64 Metadata: pkg.PythonPackage{ 65 Name: "Pygments", 66 Version: "2.6.1", 67 Platform: "any", 68 Author: "Georg Brandl", 69 AuthorEmail: "georg@python.org", 70 SitePackagesRootPath: "dist-name", 71 Files: []pkg.PythonFileRecord{ 72 {Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"}, 73 {Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"}, 74 {Path: "Pygments-2.6.1.dist-info/LICENSE.txt", Digest: &pkg.PythonFileDigest{Algorithm: "sha256", Value: "utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY"}, Size: "8449"}, 75 {Path: "Pygments-2.6.1.dist-info/RECORD"}, 76 {Path: "pygments/__pycache__/__init__.cpython-38.pyc"}, 77 {Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"}, 78 79 {Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"}, 80 }, 81 TopLevelPackages: []string{"pygments", "something_else"}, 82 DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{URL: "https://github.com/python-test/test.git", VCS: "git", CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, 83 RequiresPython: ">=3.5", 84 RequiresDist: []string{"soupsieve (>1.2)", "html5lib ; extra == 'html5lib'", "lxml ; extra == 'lxml'"}, 85 ProvidesExtra: []string{"html5lib", "lxml"}, 86 }, 87 }, 88 { 89 Name: "requests", 90 Version: "2.22.0", 91 PURL: "pkg:pypi/requests@2.22.0", 92 Type: pkg.PythonPkg, 93 Language: pkg.Python, 94 Locations: file.NewLocationSet( 95 file.NewLocation("egg-name/egg-info/PKG-INFO"), 96 file.NewLocation("egg-name/egg-info/RECORD"), 97 file.NewLocation("egg-name/egg-info/top_level.txt"), 98 ), 99 Licenses: pkg.NewLicenseSet( 100 pkg.NewLicenseFromLocationsWithContext(ctx, "Apache 2.0", file.NewLocation("egg-name/egg-info/PKG-INFO")), 101 ), 102 FoundBy: "python-installed-package-cataloger", 103 Metadata: pkg.PythonPackage{ 104 Name: "requests", 105 Version: "2.22.0", 106 Platform: "UNKNOWN", 107 Author: "Kenneth Reitz", 108 AuthorEmail: "me@kennethreitz.org", 109 SitePackagesRootPath: "egg-name", 110 Files: []pkg.PythonFileRecord{ 111 {Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"}, 112 {Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"}, 113 {Path: "requests/__pycache__/__version__.cpython-38.pyc"}, 114 {Path: "requests/__pycache__/utils.cpython-38.pyc"}, 115 {Path: "requests/__version__.py", Digest: &pkg.PythonFileDigest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"}, 116 {Path: "requests/utils.py", Digest: &pkg.PythonFileDigest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"}, 117 }, 118 TopLevelPackages: []string{"requests"}, 119 RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", 120 ProvidesExtra: []string{"security", "socks"}, 121 }, 122 }, 123 }, 124 }, 125 { 126 name: "DIST-INFO+EGG-INFO site-packages directory (case insensitive)", 127 fixture: "test-fixtures/site-packages/uppercase", 128 expectedPackages: []pkg.Package{ 129 { 130 Name: "pygments", 131 Version: "2.6.1", 132 PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 133 Type: pkg.PythonPkg, 134 Language: pkg.Python, 135 Locations: file.NewLocationSet( 136 file.NewLocation("dist-name/DIST-INFO/METADATA"), 137 file.NewLocation("dist-name/DIST-INFO/RECORD"), 138 file.NewLocation("dist-name/DIST-INFO/direct_url.json"), 139 file.NewLocation("dist-name/DIST-INFO/top_level.txt"), 140 ), 141 Licenses: pkg.NewLicenseSet( 142 // here we only used the license that was declared in the METADATA file, we did not go searching for other licenses 143 // this is the better source of truth when there is no explicit LicenseFile given 144 pkg.NewLicenseFromLocationsWithContext(ctx, "BSD License", file.NewLocation("dist-name/DIST-INFO/METADATA")), 145 ), 146 FoundBy: "python-installed-package-cataloger", 147 Metadata: pkg.PythonPackage{ 148 Name: "Pygments", 149 Version: "2.6.1", 150 Platform: "any", 151 Author: "Georg Brandl", 152 AuthorEmail: "georg@python.org", 153 SitePackagesRootPath: "dist-name", 154 Files: []pkg.PythonFileRecord{ 155 {Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"}, 156 {Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"}, 157 {Path: "Pygments-2.6.1.dist-info/RECORD"}, 158 {Path: "pygments/__pycache__/__init__.cpython-38.pyc"}, 159 {Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"}, 160 161 {Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"}, 162 }, 163 TopLevelPackages: []string{"pygments", "something_else"}, 164 DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{URL: "https://github.com/python-test/test.git", VCS: "git", CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, 165 RequiresPython: ">=3.5", 166 }, 167 }, 168 { 169 Name: "requests", 170 Version: "2.22.0", 171 PURL: "pkg:pypi/requests@2.22.0", 172 Type: pkg.PythonPkg, 173 Language: pkg.Python, 174 Locations: file.NewLocationSet( 175 file.NewLocation("egg-name/EGG-INFO/PKG-INFO"), 176 file.NewLocation("egg-name/EGG-INFO/RECORD"), 177 file.NewLocation("egg-name/EGG-INFO/top_level.txt"), 178 ), 179 Licenses: pkg.NewLicenseSet( 180 pkg.NewLicenseFromLocationsWithContext(ctx, "Apache 2.0", file.NewLocation("egg-name/EGG-INFO/PKG-INFO")), 181 ), 182 FoundBy: "python-installed-package-cataloger", 183 Metadata: pkg.PythonPackage{ 184 Name: "requests", 185 Version: "2.22.0", 186 Platform: "UNKNOWN", 187 Author: "Kenneth Reitz", 188 AuthorEmail: "me@kennethreitz.org", 189 SitePackagesRootPath: "egg-name", 190 Files: []pkg.PythonFileRecord{ 191 {Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"}, 192 {Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"}, 193 {Path: "requests/__pycache__/__version__.cpython-38.pyc"}, 194 {Path: "requests/__pycache__/utils.cpython-38.pyc"}, 195 {Path: "requests/__version__.py", Digest: &pkg.PythonFileDigest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"}, 196 {Path: "requests/utils.py", Digest: &pkg.PythonFileDigest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"}, 197 }, 198 TopLevelPackages: []string{"requests"}, 199 RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", 200 ProvidesExtra: []string{"security", "socks"}, 201 }, 202 }, 203 }, 204 }, 205 { 206 name: "detect licenses", 207 fixture: "test-fixtures/site-packages/license", 208 expectedPackages: []pkg.Package{ 209 { 210 Name: "pygments", 211 Version: "2.6.1", 212 PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 213 Type: pkg.PythonPkg, 214 Language: pkg.Python, 215 Locations: file.NewLocationSet( 216 file.NewLocation("with-license-file-declared.dist-info/METADATA"), // the LicenseFile is declared in the METADATA file 217 file.NewLocation("with-license-file-declared.dist-info/RECORD"), 218 file.NewLocation("with-license-file-declared.dist-info/top_level.txt"), 219 file.NewLocation("with-license-file-declared.dist-info/direct_url.json"), 220 ), 221 Licenses: pkg.NewLicenseSet( 222 pkg.License{ 223 Value: "BSD-3-Clause", 224 SPDXExpression: "BSD-3-Clause", 225 Type: "concluded", 226 Contents: mustContentsFromLocation(t, "test-fixtures/site-packages/license/with-license-file-declared.dist-info/LICENSE.txt", 0, 1475), 227 // we read the path from the LicenseFile field in the METADATA file, then read the license file directly 228 Locations: file.NewLocationSet(file.NewLocation("with-license-file-declared.dist-info/LICENSE.txt")), 229 }, 230 ), 231 FoundBy: "python-installed-package-cataloger", 232 Metadata: pkg.PythonPackage{ 233 Name: "Pygments", 234 Version: "2.6.1", 235 Platform: "any", 236 Author: "Georg Brandl", 237 AuthorEmail: "georg@python.org", 238 SitePackagesRootPath: ".", 239 Files: []pkg.PythonFileRecord{ 240 {Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"}, 241 {Path: "with-license-file-declared.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"}, 242 {Path: "with-license-file-declared.dist-info/LICENSE.txt", Digest: &pkg.PythonFileDigest{Algorithm: "sha256", Value: "utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY"}, Size: "8449"}, 243 {Path: "with-license-file-declared.dist-info/RECORD"}, 244 {Path: "pygments/__pycache__/__init__.cpython-38.pyc"}, 245 {Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"}, 246 247 {Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"}, 248 }, 249 TopLevelPackages: []string{"pygments", "something_else"}, 250 DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{URL: "https://github.com/python-test/test.git", VCS: "git", CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, 251 RequiresPython: ">=3.5", 252 RequiresDist: []string{"soupsieve (>1.2)", "html5lib ; extra == 'html5lib'", "lxml ; extra == 'lxml'"}, 253 ProvidesExtra: []string{"html5lib", "lxml"}, 254 }, 255 }, 256 { 257 Name: "pygments", 258 Version: "2.6.1", 259 PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 260 Type: pkg.PythonPkg, 261 Language: pkg.Python, 262 Locations: file.NewLocationSet( 263 file.NewLocation("without-license-file-declared.dist-info/METADATA"), // the LicenseFile is declared in the METADATA file 264 file.NewLocation("without-license-file-declared.dist-info/RECORD"), 265 file.NewLocation("without-license-file-declared.dist-info/top_level.txt"), 266 file.NewLocation("without-license-file-declared.dist-info/direct_url.json"), 267 ), 268 Licenses: pkg.NewLicenseSet( 269 pkg.License{ 270 Value: "BSD-3-Clause", 271 SPDXExpression: "BSD-3-Clause", 272 Type: "concluded", 273 Contents: mustContentsFromLocation(t, "test-fixtures/site-packages/license/with-license-file-declared.dist-info/LICENSE.txt", 0, 1475), 274 Locations: file.NewLocationSet(file.NewLocation("without-license-file-declared.dist-info/LICENSE.txt")), 275 }, 276 ), 277 FoundBy: "python-installed-package-cataloger", 278 Metadata: pkg.PythonPackage{ 279 Name: "Pygments", 280 Version: "2.6.1", 281 Platform: "any", 282 Author: "Georg Brandl", 283 AuthorEmail: "georg@python.org", 284 SitePackagesRootPath: ".", 285 Files: []pkg.PythonFileRecord{ 286 {Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"}, 287 {Path: "without-license-file-declared.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"}, 288 {Path: "without-license-file-declared.dist-info/LICENSE.txt", Digest: &pkg.PythonFileDigest{Algorithm: "sha256", Value: "utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY"}, Size: "8449"}, 289 {Path: "without-license-file-declared.dist-info/RECORD"}, 290 {Path: "pygments/__pycache__/__init__.cpython-38.pyc"}, 291 {Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"}, 292 293 {Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"}, 294 }, 295 TopLevelPackages: []string{"pygments", "something_else"}, 296 DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{URL: "https://github.com/python-test/test.git", VCS: "git", CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"}, 297 RequiresPython: ">=3.5", 298 RequiresDist: []string{"soupsieve (>1.2)", "html5lib ; extra == 'html5lib'", "lxml ; extra == 'lxml'"}, 299 ProvidesExtra: []string{"html5lib", "lxml"}, 300 }, 301 }, 302 }, 303 }, 304 { 305 name: "malformed-record", 306 fixture: "test-fixtures/site-packages/malformed-record", 307 expectedPackages: []pkg.Package{ 308 { 309 Name: "pygments", 310 Version: "2.6.1", 311 PURL: "pkg:pypi/pygments@2.6.1", 312 Type: pkg.PythonPkg, 313 Language: pkg.Python, 314 Locations: file.NewLocationSet( 315 file.NewLocation("dist-info/METADATA"), 316 file.NewLocation("dist-info/RECORD"), 317 ), 318 Licenses: pkg.NewLicenseSet( 319 pkg.NewLicenseFromLocationsWithContext(ctx, "BSD License", file.NewLocation("dist-info/METADATA")), 320 ), 321 FoundBy: "python-installed-package-cataloger", 322 Metadata: pkg.PythonPackage{ 323 Name: "Pygments", 324 Version: "2.6.1", 325 Platform: "any", 326 Author: "Georg Brandl", 327 AuthorEmail: "georg@python.org", 328 SitePackagesRootPath: ".", 329 Files: []pkg.PythonFileRecord{ 330 {Path: "flask/json/tag.py", Digest: &pkg.PythonFileDigest{"sha256", "9ehzrmt5k7hxf7ZEK0NOs3swvQyU9fWNe-pnYe69N60"}, Size: "8223"}, 331 {Path: "../../Scripts/flask.exe", Digest: &pkg.PythonFileDigest{"sha256", "mPrbVeZCDX20himZ_bRai1nCs_tgr7jHIOGZlcgn-T4"}, Size: "93063"}, 332 {Path: "../../Scripts/flask.exe", Size: "89470", Digest: &pkg.PythonFileDigest{"sha256", "jvqh4N3qOqXLlq40i6ZOLCY9tAOwfwdzIpLDYhRjoqQ"}}, 333 {Path: "Flask-1.0.2.dist-info/INSTALLER", Size: "4", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}}, 334 }, 335 RequiresPython: ">=3.5", 336 }, 337 }, 338 }, 339 }, 340 { 341 // in cases where the metadata file is available and the record is not we should still record there is a package 342 // additionally empty top_level.txt files should not result in an error 343 name: "partial dist-info directory", 344 fixture: "test-fixtures/site-packages/partial.dist-info", 345 expectedPackages: []pkg.Package{ 346 { 347 Name: "pygments", 348 Version: "2.6.1", 349 PURL: "pkg:pypi/pygments@2.6.1", 350 Type: pkg.PythonPkg, 351 Language: pkg.Python, 352 Locations: file.NewLocationSet( 353 file.NewLocation("METADATA"), 354 ), 355 Licenses: pkg.NewLicenseSet( 356 pkg.NewLicenseFromLocationsWithContext(ctx, "BSD License", file.NewLocation("METADATA")), 357 ), 358 FoundBy: "python-installed-package-cataloger", 359 Metadata: pkg.PythonPackage{ 360 Name: "Pygments", 361 Version: "2.6.1", 362 Platform: "any", 363 Author: "Georg Brandl", 364 AuthorEmail: "georg@python.org", 365 SitePackagesRootPath: ".", 366 RequiresPython: ">=3.5", 367 }, 368 }, 369 }, 370 }, 371 { 372 name: "egg-info regular file", 373 fixture: "test-fixtures/site-packages/test", 374 expectedPackages: []pkg.Package{ 375 { 376 Name: "requests", 377 Version: "2.22.0", 378 PURL: "pkg:pypi/requests@2.22.0", 379 Type: pkg.PythonPkg, 380 Language: pkg.Python, 381 Locations: file.NewLocationSet( 382 file.NewLocation("test.egg-info"), 383 ), 384 Licenses: pkg.NewLicenseSet( 385 pkg.NewLicenseFromLocationsWithContext(ctx, "Apache 2.0", file.NewLocation("test.egg-info")), 386 ), 387 FoundBy: "python-installed-package-cataloger", 388 Metadata: pkg.PythonPackage{ 389 Name: "requests", 390 Version: "2.22.0", 391 Platform: "UNKNOWN", 392 Author: "Kenneth Reitz", 393 AuthorEmail: "me@kennethreitz.org", 394 SitePackagesRootPath: ".", 395 RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", 396 ProvidesExtra: []string{"security", "socks"}, 397 }, 398 }, 399 }, 400 }, 401 } 402 403 for _, test := range tests { 404 t.Run(test.name, func(t *testing.T) { 405 (pkgtest.NewCatalogTester(). 406 FromDirectory(t, test.fixture). 407 Expects(test.expectedPackages, nil). 408 TestCataloger(t, NewInstalledPackageCataloger())) 409 }) 410 } 411 } 412 413 func Test_PackageCataloger_IgnorePackage(t *testing.T) { 414 tests := []struct { 415 MetadataFixture string 416 }{ 417 { 418 MetadataFixture: "test-fixtures/Python-2.7.egg-info", 419 }, 420 { 421 MetadataFixture: "test-fixtures/empty-1.0.0-py3.8.egg-info", 422 }, 423 } 424 425 for _, test := range tests { 426 t.Run(test.MetadataFixture, func(t *testing.T) { 427 resolver := file.NewMockResolverForPaths(test.MetadataFixture) 428 429 actual, _, err := NewInstalledPackageCataloger().Catalog(pkgtest.Context(), resolver) 430 require.NoError(t, err) 431 432 if len(actual) != 0 { 433 t.Fatalf("Expected 0 packages but found: %d", len(actual)) 434 } 435 }) 436 } 437 } 438 439 func Test_IndexCataloger_Globs(t *testing.T) { 440 tests := []struct { 441 name string 442 fixture string 443 expected []string 444 }{ 445 { 446 name: "obtain index files", 447 fixture: "test-fixtures/glob-paths", 448 expected: []string{ 449 "src/requirements.txt", 450 "src/extra-requirements.txt", 451 "src/requirements-dev.txt", 452 "src/1-requirements-dev.txt", 453 "src/setup.py", 454 "src/poetry.lock", 455 "src/Pipfile.lock", 456 "src/uv.lock", 457 "src/pdm.lock", 458 }, 459 }, 460 } 461 462 for _, test := range tests { 463 t.Run(test.name, func(t *testing.T) { 464 pkgtest.NewCatalogTester(). 465 FromDirectory(t, test.fixture). 466 ExpectsResolverContentQueries(test.expected). 467 TestCataloger(t, NewPackageCataloger(DefaultCatalogerConfig())) 468 }) 469 } 470 } 471 472 func Test_PackageCataloger_Globs(t *testing.T) { 473 tests := []struct { 474 name string 475 fixture string 476 expected []string 477 }{ 478 { 479 name: "obtain index files", 480 fixture: "test-fixtures/glob-paths", 481 expected: []string{ 482 "site-packages/v.DIST-INFO/METADATA", 483 "site-packages/w.EGG-INFO/PKG-INFO", 484 "site-packages/x.dist-info/METADATA", 485 "site-packages/y.egg-info/PKG-INFO", 486 "site-packages/z.egg-info", 487 }, 488 }, 489 } 490 491 for _, test := range tests { 492 t.Run(test.name, func(t *testing.T) { 493 pkgtest.NewCatalogTester(). 494 FromDirectory(t, test.fixture). 495 ExpectsResolverContentQueries(test.expected). 496 IgnoreUnfulfilledPathResponses("**/pyvenv.cfg"). 497 TestCataloger(t, NewInstalledPackageCataloger()) 498 }) 499 } 500 } 501 502 func Test_PackageCataloger_Relationships(t *testing.T) { 503 tests := []struct { 504 name string 505 fixture string 506 expectedRelationships []string 507 }{ 508 { 509 name: "poetry - no dependencies", 510 fixture: "test-fixtures/poetry/dev-deps", 511 expectedRelationships: nil, 512 }, 513 { 514 name: "poetry - simple dependencies", 515 fixture: "test-fixtures/poetry/simple-deps", 516 expectedRelationships: []string{ 517 "certifi @ 2024.2.2 (.) [dependency-of] requests @ 2.32.2 (.)", 518 "charset-normalizer @ 3.3.2 (.) [dependency-of] requests @ 2.32.2 (.)", 519 "idna @ 3.7 (.) [dependency-of] requests @ 2.32.2 (.)", 520 "urllib3 @ 2.2.1 (.) [dependency-of] requests @ 2.32.2 (.)", 521 }, 522 }, 523 { 524 name: "poetry - multiple extras", 525 fixture: "test-fixtures/poetry/multiple-extras", 526 expectedRelationships: []string{ 527 "anyio @ 4.3.0 (.) [dependency-of] anyio @ 4.3.0 (.)", 528 "anyio @ 4.3.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 529 "anyio @ 4.3.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 530 "brotli @ 1.1.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 531 "brotlicffi @ 1.1.0.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 532 "certifi @ 2024.2.2 (.) [dependency-of] httpcore @ 1.0.5 (.)", 533 "certifi @ 2024.2.2 (.) [dependency-of] httpx @ 0.27.0 (.)", 534 "cffi @ 1.16.0 (.) [dependency-of] brotlicffi @ 1.1.0.0 (.)", 535 "h11 @ 0.14.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 536 "h2 @ 4.1.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 537 "h2 @ 4.1.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 538 "hpack @ 4.0.0 (.) [dependency-of] h2 @ 4.1.0 (.)", 539 "httpcore @ 1.0.5 (.) [dependency-of] httpx @ 0.27.0 (.)", 540 "hyperframe @ 6.0.1 (.) [dependency-of] h2 @ 4.1.0 (.)", 541 "idna @ 3.7 (.) [dependency-of] anyio @ 4.3.0 (.)", 542 "idna @ 3.7 (.) [dependency-of] httpx @ 0.27.0 (.)", 543 "pycparser @ 2.22 (.) [dependency-of] cffi @ 1.16.0 (.)", 544 "sniffio @ 1.3.1 (.) [dependency-of] anyio @ 4.3.0 (.)", 545 "sniffio @ 1.3.1 (.) [dependency-of] httpx @ 0.27.0 (.)", 546 "socksio @ 1.0.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 547 "socksio @ 1.0.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 548 }, 549 }, 550 { 551 name: "poetry - nested extras", 552 fixture: "test-fixtures/poetry/nested-extras", 553 expectedRelationships: []string{ 554 "annotated-types @ 0.7.0 (.) [dependency-of] pydantic @ 2.7.1 (.)", 555 "anyio @ 4.3.0 (.) [dependency-of] anyio @ 4.3.0 (.)", 556 "anyio @ 4.3.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 557 "anyio @ 4.3.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 558 "anyio @ 4.3.0 (.) [dependency-of] starlette @ 0.37.2 (.)", 559 "anyio @ 4.3.0 (.) [dependency-of] watchfiles @ 0.21.0 (.)", 560 "certifi @ 2024.2.2 (.) [dependency-of] httpcore @ 1.0.5 (.)", 561 "certifi @ 2024.2.2 (.) [dependency-of] httpx @ 0.27.0 (.)", 562 "click @ 8.1.7 (.) [dependency-of] httpx @ 0.27.0 (.)", 563 "click @ 8.1.7 (.) [dependency-of] python-dotenv @ 1.0.1 (.)", 564 "click @ 8.1.7 (.) [dependency-of] typer @ 0.12.3 (.)", 565 "click @ 8.1.7 (.) [dependency-of] uvicorn @ 0.29.0 (.)", 566 "colorama @ 0.4.6 (.) [dependency-of] click @ 8.1.7 (.)", 567 "colorama @ 0.4.6 (.) [dependency-of] pygments @ 2.18.0 (.)", 568 "colorama @ 0.4.6 (.) [dependency-of] uvicorn @ 0.29.0 (.)", // proof of uvicorn[standard] 569 "dnspython @ 2.6.1 (.) [dependency-of] email-validator @ 2.1.1 (.)", 570 "email-validator @ 2.1.1 (.) [dependency-of] fastapi @ 0.111.0 (.)", 571 "email-validator @ 2.1.1 (.) [dependency-of] pydantic @ 2.7.1 (.)", 572 "fastapi @ 0.111.0 (.) [dependency-of] fastapi-cli @ 0.0.4 (.)", 573 "fastapi-cli @ 0.0.4 (.) [dependency-of] fastapi @ 0.111.0 (.)", 574 "h11 @ 0.14.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 575 "h11 @ 0.14.0 (.) [dependency-of] uvicorn @ 0.29.0 (.)", 576 "httpcore @ 1.0.5 (.) [dependency-of] dnspython @ 2.6.1 (.)", 577 "httpcore @ 1.0.5 (.) [dependency-of] httpx @ 0.27.0 (.)", 578 "httptools @ 0.6.1 (.) [dependency-of] uvicorn @ 0.29.0 (.)", // proof of uvicorn[standard] 579 "httpx @ 0.27.0 (.) [dependency-of] dnspython @ 2.6.1 (.)", 580 "httpx @ 0.27.0 (.) [dependency-of] fastapi @ 0.111.0 (.)", 581 "httpx @ 0.27.0 (.) [dependency-of] starlette @ 0.37.2 (.)", 582 "idna @ 3.7 (.) [dependency-of] anyio @ 4.3.0 (.)", 583 "idna @ 3.7 (.) [dependency-of] dnspython @ 2.6.1 (.)", 584 "idna @ 3.7 (.) [dependency-of] email-validator @ 2.1.1 (.)", 585 "idna @ 3.7 (.) [dependency-of] httpx @ 0.27.0 (.)", 586 "itsdangerous @ 2.2.0 (.) [dependency-of] fastapi @ 0.111.0 (.)", 587 "itsdangerous @ 2.2.0 (.) [dependency-of] starlette @ 0.37.2 (.)", 588 "jinja2 @ 3.1.4 (.) [dependency-of] fastapi @ 0.111.0 (.)", 589 "jinja2 @ 3.1.4 (.) [dependency-of] starlette @ 0.37.2 (.)", 590 "markdown-it-py @ 3.0.0 (.) [dependency-of] rich @ 13.7.1 (.)", 591 "mdurl @ 0.1.2 (.) [dependency-of] markdown-it-py @ 3.0.0 (.)", 592 "orjson @ 3.10.3 (.) [dependency-of] fastapi @ 0.111.0 (.)", 593 "pydantic @ 2.7.1 (.) [dependency-of] fastapi @ 0.111.0 (.)", 594 "pydantic @ 2.7.1 (.) [dependency-of] pydantic-extra-types @ 2.7.0 (.)", 595 "pydantic @ 2.7.1 (.) [dependency-of] pydantic-settings @ 2.2.1 (.)", 596 "pydantic-core @ 2.18.2 (.) [dependency-of] pydantic @ 2.7.1 (.)", 597 "pydantic-extra-types @ 2.7.0 (.) [dependency-of] fastapi @ 0.111.0 (.)", 598 "pydantic-settings @ 2.2.1 (.) [dependency-of] fastapi @ 0.111.0 (.)", 599 "pygments @ 2.18.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 600 "pygments @ 2.18.0 (.) [dependency-of] rich @ 13.7.1 (.)", 601 "python-dotenv @ 1.0.1 (.) [dependency-of] pydantic-settings @ 2.2.1 (.)", 602 "python-dotenv @ 1.0.1 (.) [dependency-of] uvicorn @ 0.29.0 (.)", // proof of uvicorn[standard] 603 "python-multipart @ 0.0.9 (.) [dependency-of] fastapi @ 0.111.0 (.)", 604 "python-multipart @ 0.0.9 (.) [dependency-of] starlette @ 0.37.2 (.)", 605 "pyyaml @ 6.0.1 (.) [dependency-of] fastapi @ 0.111.0 (.)", 606 "pyyaml @ 6.0.1 (.) [dependency-of] markdown-it-py @ 3.0.0 (.)", 607 "pyyaml @ 6.0.1 (.) [dependency-of] pydantic-settings @ 2.2.1 (.)", 608 "pyyaml @ 6.0.1 (.) [dependency-of] python-multipart @ 0.0.9 (.)", 609 "pyyaml @ 6.0.1 (.) [dependency-of] starlette @ 0.37.2 (.)", 610 "pyyaml @ 6.0.1 (.) [dependency-of] uvicorn @ 0.29.0 (.)", // proof of uvicorn[standard] 611 "rich @ 13.7.1 (.) [dependency-of] httpx @ 0.27.0 (.)", 612 "rich @ 13.7.1 (.) [dependency-of] typer @ 0.12.3 (.)", 613 "shellingham @ 1.5.4 (.) [dependency-of] typer @ 0.12.3 (.)", 614 "sniffio @ 1.3.1 (.) [dependency-of] anyio @ 4.3.0 (.)", 615 "sniffio @ 1.3.1 (.) [dependency-of] httpx @ 0.27.0 (.)", 616 "starlette @ 0.37.2 (.) [dependency-of] fastapi @ 0.111.0 (.)", 617 "typer @ 0.12.3 (.) [dependency-of] fastapi-cli @ 0.0.4 (.)", 618 "typing-extensions @ 4.12.0 (.) [dependency-of] fastapi @ 0.111.0 (.)", 619 "typing-extensions @ 4.12.0 (.) [dependency-of] pydantic @ 2.7.1 (.)", 620 "typing-extensions @ 4.12.0 (.) [dependency-of] pydantic-core @ 2.18.2 (.)", 621 "typing-extensions @ 4.12.0 (.) [dependency-of] typer @ 0.12.3 (.)", 622 "ujson @ 5.10.0 (.) [dependency-of] fastapi @ 0.111.0 (.)", 623 "uvicorn @ 0.29.0 (.) [dependency-of] fastapi @ 0.111.0 (.)", 624 "uvicorn @ 0.29.0 (.) [dependency-of] fastapi-cli @ 0.0.4 (.)", 625 "uvloop @ 0.19.0 (.) [dependency-of] anyio @ 4.3.0 (.)", 626 "uvloop @ 0.19.0 (.) [dependency-of] uvicorn @ 0.29.0 (.)", // proof of uvicorn[standard] 627 "watchfiles @ 0.21.0 (.) [dependency-of] uvicorn @ 0.29.0 (.)", // proof of uvicorn[standard] 628 "websockets @ 12.0 (.) [dependency-of] uvicorn @ 0.29.0 (.)", // proof of uvicorn[standard] 629 }, 630 }, 631 { 632 name: "poetry - conflicting extras", 633 fixture: "test-fixtures/poetry/conflicting-with-extras", 634 expectedRelationships: []string{ 635 "anyio @ 4.3.0 (.) [dependency-of] anyio @ 4.3.0 (.)", 636 "anyio @ 4.3.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 637 "anyio @ 4.3.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 638 "brotli @ 1.1.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 639 "brotlicffi @ 1.1.0.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 640 "certifi @ 2024.2.2 (.) [dependency-of] httpcore @ 1.0.5 (.)", 641 "certifi @ 2024.2.2 (.) [dependency-of] httpx @ 0.27.0 (.)", 642 "cffi @ 1.16.0 (.) [dependency-of] brotlicffi @ 1.1.0.0 (.)", 643 "colorama @ 0.4.6 (.) [dependency-of] rich @ 0.3.3 (.)", 644 "h11 @ 0.14.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 645 "h2 @ 4.1.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 646 "h2 @ 4.1.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 647 "hpack @ 4.0.0 (.) [dependency-of] h2 @ 4.1.0 (.)", 648 "httpcore @ 1.0.5 (.) [dependency-of] httpx @ 0.27.0 (.)", 649 "hyperframe @ 6.0.1 (.) [dependency-of] h2 @ 4.1.0 (.)", 650 "idna @ 3.7 (.) [dependency-of] anyio @ 4.3.0 (.)", 651 "idna @ 3.7 (.) [dependency-of] httpx @ 0.27.0 (.)", 652 "pprintpp @ 0.4.0 (.) [dependency-of] rich @ 0.3.3 (.)", 653 "pycparser @ 2.22 (.) [dependency-of] cffi @ 1.16.0 (.)", 654 "sniffio @ 1.3.1 (.) [dependency-of] anyio @ 4.3.0 (.)", 655 "sniffio @ 1.3.1 (.) [dependency-of] httpx @ 0.27.0 (.)", 656 "socksio @ 1.0.0 (.) [dependency-of] httpcore @ 1.0.5 (.)", 657 "socksio @ 1.0.0 (.) [dependency-of] httpx @ 0.27.0 (.)", 658 "typing-extensions @ 3.10.0.2 (.) [dependency-of] rich @ 0.3.3 (.)", 659 660 // ideally we should NOT see these dependencies. However, they are technically installed in the environment 661 // and an import is present in httpx for each of these, so in theory they are actually dependencies even 662 // though our pyproject.toml looks like this: 663 // 664 // [tool.poetry.dependencies] 665 // python = "^3.11" 666 // httpx = {extras = ["brotli", "http2", "socks"], version = "^0.27.0"} 667 // pygments = "1.6" 668 // click = "<8" 669 // rich = "<10" 670 // 671 // note that pygments, click, and rich are installed outside of the allowable ranges for the given 672 // httpx package version constraints, per the poetry.lock: 673 // 674 // # for package httpx 675 // [package.extras] 676 // cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 677 // 678 // note: the pyproject.toml and poetry.lock state are consistent with each other (just with 679 // "poetry install" and "poetry lock", and nothing was forced!) 680 "click @ 7.1.2 (.) [dependency-of] httpx @ 0.27.0 (.)", 681 "pygments @ 1.6 (.) [dependency-of] httpx @ 0.27.0 (.)", 682 "rich @ 0.3.3 (.) [dependency-of] httpx @ 0.27.0 (.)", 683 }, 684 }, 685 { 686 name: "uv - simple dependencies", 687 fixture: "test-fixtures/uv/simple-deps", 688 expectedRelationships: []string{ 689 "certifi @ 2025.1.31 (.) [dependency-of] requests @ 2.32.3 (.)", 690 "charset-normalizer @ 3.4.1 (.) [dependency-of] requests @ 2.32.3 (.)", 691 "idna @ 3.10 (.) [dependency-of] requests @ 2.32.3 (.)", 692 "requests @ 2.32.3 (.) [dependency-of] testpkg @ 0.1.0 (.)", 693 "urllib3 @ 2.3.0 (.) [dependency-of] requests @ 2.32.3 (.)", 694 }, 695 }, 696 { 697 name: "uv - multiple extras", 698 fixture: "test-fixtures/uv/multiple-extras", 699 expectedRelationships: []string{ 700 "anyio @ 4.9.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 701 "brotli @ 1.1.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 702 "brotlicffi @ 1.1.0.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 703 "certifi @ 2025.1.31 (.) [dependency-of] httpcore @ 1.0.7 (.)", 704 "certifi @ 2025.1.31 (.) [dependency-of] httpx @ 0.28.1 (.)", 705 "cffi @ 1.17.1 (.) [dependency-of] brotlicffi @ 1.1.0.0 (.)", 706 "h11 @ 0.14.0 (.) [dependency-of] httpcore @ 1.0.7 (.)", 707 "h2 @ 4.2.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 708 "hpack @ 4.1.0 (.) [dependency-of] h2 @ 4.2.0 (.)", 709 "httpcore @ 1.0.7 (.) [dependency-of] httpx @ 0.28.1 (.)", 710 "httpx @ 0.28.1 (.) [dependency-of] testpkg @ 0.1.0 (.)", 711 "hyperframe @ 6.1.0 (.) [dependency-of] h2 @ 4.2.0 (.)", 712 "idna @ 3.10 (.) [dependency-of] anyio @ 4.9.0 (.)", 713 "idna @ 3.10 (.) [dependency-of] httpx @ 0.28.1 (.)", 714 "pycparser @ 2.22 (.) [dependency-of] cffi @ 1.17.1 (.)", 715 "sniffio @ 1.3.1 (.) [dependency-of] anyio @ 4.9.0 (.)", 716 "socksio @ 1.0.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 717 "typing-extensions @ 4.13.0 (.) [dependency-of] anyio @ 4.9.0 (.)", 718 }, 719 }, 720 { 721 name: "uv - nested extras", 722 fixture: "test-fixtures/uv/nested-extras", 723 expectedRelationships: []string{ 724 "annotated-types @ 0.7.0 (.) [dependency-of] pydantic @ 2.11.0 (.)", 725 "anyio @ 4.9.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 726 "anyio @ 4.9.0 (.) [dependency-of] starlette @ 0.37.2 (.)", 727 "anyio @ 4.9.0 (.) [dependency-of] watchfiles @ 1.0.4 (.)", 728 "certifi @ 2025.1.31 (.) [dependency-of] httpcore @ 1.0.7 (.)", 729 "certifi @ 2025.1.31 (.) [dependency-of] httpx @ 0.28.1 (.)", 730 "click @ 8.1.8 (.) [dependency-of] rich-toolkit @ 0.14.0 (.)", 731 "click @ 8.1.8 (.) [dependency-of] typer @ 0.15.2 (.)", 732 "click @ 8.1.8 (.) [dependency-of] uvicorn @ 0.34.0 (.)", 733 "colorama @ 0.4.6 (.) [dependency-of] click @ 8.1.8 (.)", 734 "colorama @ 0.4.6 (.) [dependency-of] uvicorn @ 0.34.0 (.)", // proof of uvicorn[standard] 735 "dnspython @ 2.7.0 (.) [dependency-of] email-validator @ 2.2.0 (.)", 736 "email-validator @ 2.2.0 (.) [dependency-of] fastapi @ 0.111.1 (.)", 737 "fastapi @ 0.111.1 (.) [dependency-of] testpkg @ 0.1.0 (.)", 738 "fastapi-cli @ 0.0.7 (.) [dependency-of] fastapi @ 0.111.1 (.)", 739 "h11 @ 0.14.0 (.) [dependency-of] httpcore @ 1.0.7 (.)", 740 "h11 @ 0.14.0 (.) [dependency-of] uvicorn @ 0.34.0 (.)", 741 "httpcore @ 1.0.7 (.) [dependency-of] httpx @ 0.28.1 (.)", 742 "httptools @ 0.6.4 (.) [dependency-of] uvicorn @ 0.34.0 (.)", // proof of uvicorn[standard] 743 "httpx @ 0.28.1 (.) [dependency-of] fastapi @ 0.111.1 (.)", 744 "idna @ 3.10 (.) [dependency-of] anyio @ 4.9.0 (.)", 745 "idna @ 3.10 (.) [dependency-of] email-validator @ 2.2.0 (.)", 746 "idna @ 3.10 (.) [dependency-of] httpx @ 0.28.1 (.)", 747 "itsdangerous @ 2.2.0 (.) [dependency-of] fastapi @ 0.111.1 (.)", 748 "jinja2 @ 3.1.6 (.) [dependency-of] fastapi @ 0.111.1 (.)", 749 "markdown-it-py @ 3.0.0 (.) [dependency-of] rich @ 13.9.4 (.)", 750 "markupsafe @ 3.0.2 (.) [dependency-of] jinja2 @ 3.1.6 (.)", 751 "mdurl @ 0.1.2 (.) [dependency-of] markdown-it-py @ 3.0.0 (.)", 752 "orjson @ 3.10.16 (.) [dependency-of] fastapi @ 0.111.1 (.)", 753 "pydantic @ 2.11.0 (.) [dependency-of] fastapi @ 0.111.1 (.)", 754 "pydantic @ 2.11.0 (.) [dependency-of] pydantic-extra-types @ 2.10.3 (.)", 755 "pydantic @ 2.11.0 (.) [dependency-of] pydantic-settings @ 2.8.1 (.)", 756 "pydantic-core @ 2.33.0 (.) [dependency-of] pydantic @ 2.11.0 (.)", 757 "pydantic-extra-types @ 2.10.3 (.) [dependency-of] fastapi @ 0.111.1 (.)", 758 "pydantic-settings @ 2.8.1 (.) [dependency-of] fastapi @ 0.111.1 (.)", 759 "pygments @ 2.19.1 (.) [dependency-of] rich @ 13.9.4 (.)", 760 "python-dotenv @ 1.1.0 (.) [dependency-of] pydantic-settings @ 2.8.1 (.)", 761 "python-dotenv @ 1.1.0 (.) [dependency-of] uvicorn @ 0.34.0 (.)", // proof of uvicorn[standard] 762 "python-multipart @ 0.0.20 (.) [dependency-of] fastapi @ 0.111.1 (.)", 763 "pyyaml @ 6.0.2 (.) [dependency-of] fastapi @ 0.111.1 (.)", 764 "pyyaml @ 6.0.2 (.) [dependency-of] uvicorn @ 0.34.0 (.)", // proof of uvicorn[standard] 765 "rich @ 13.9.4 (.) [dependency-of] rich-toolkit @ 0.14.0 (.)", 766 "rich @ 13.9.4 (.) [dependency-of] typer @ 0.15.2 (.)", 767 "rich-toolkit @ 0.14.0 (.) [dependency-of] fastapi-cli @ 0.0.7 (.)", 768 "shellingham @ 1.5.4 (.) [dependency-of] typer @ 0.15.2 (.)", 769 "sniffio @ 1.3.1 (.) [dependency-of] anyio @ 4.9.0 (.)", 770 "starlette @ 0.37.2 (.) [dependency-of] fastapi @ 0.111.1 (.)", 771 "typer @ 0.15.2 (.) [dependency-of] fastapi-cli @ 0.0.7 (.)", 772 "typing-extensions @ 4.13.0 (.) [dependency-of] anyio @ 4.9.0 (.)", 773 "typing-extensions @ 4.13.0 (.) [dependency-of] fastapi @ 0.111.1 (.)", 774 "typing-extensions @ 4.13.0 (.) [dependency-of] pydantic @ 2.11.0 (.)", 775 "typing-extensions @ 4.13.0 (.) [dependency-of] pydantic-core @ 2.33.0 (.)", 776 "typing-extensions @ 4.13.0 (.) [dependency-of] pydantic-extra-types @ 2.10.3 (.)", 777 "typing-extensions @ 4.13.0 (.) [dependency-of] rich-toolkit @ 0.14.0 (.)", 778 "typing-extensions @ 4.13.0 (.) [dependency-of] typer @ 0.15.2 (.)", 779 "typing-extensions @ 4.13.0 (.) [dependency-of] typing-inspection @ 0.4.0 (.)", 780 "typing-inspection @ 0.4.0 (.) [dependency-of] pydantic @ 2.11.0 (.)", 781 "ujson @ 5.10.0 (.) [dependency-of] fastapi @ 0.111.1 (.)", 782 "uvicorn @ 0.34.0 (.) [dependency-of] fastapi @ 0.111.1 (.)", 783 "uvicorn @ 0.34.0 (.) [dependency-of] fastapi-cli @ 0.0.7 (.)", 784 "uvloop @ 0.21.0 (.) [dependency-of] uvicorn @ 0.34.0 (.)", // proof of uvicorn[standard] 785 "watchfiles @ 1.0.4 (.) [dependency-of] uvicorn @ 0.34.0 (.)", // proof of uvicorn[standard] 786 "websockets @ 15.0.1 (.) [dependency-of] uvicorn @ 0.34.0 (.)", // proof of uvicorn[standard] 787 }, 788 }, 789 { 790 name: "uv - conflicting extras", 791 fixture: "test-fixtures/uv/conflicting-with-extras", 792 expectedRelationships: []string{ 793 "anyio @ 4.6.2.post1 (.) [dependency-of] httpx @ 0.28.1 (.)", 794 "brotli @ 1.1.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 795 "brotlicffi @ 1.1.0.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 796 "certifi @ 2025.1.31 (.) [dependency-of] httpcore @ 1.0.7 (.)", 797 "certifi @ 2025.1.31 (.) [dependency-of] httpx @ 0.28.1 (.)", 798 "cffi @ 1.17.1 (.) [dependency-of] brotlicffi @ 1.1.0.0 (.)", 799 "colorama @ 0.4.6 (.) [dependency-of] rich @ 0.3.3 (.)", 800 "h11 @ 0.14.0 (.) [dependency-of] httpcore @ 1.0.7 (.)", 801 "h2 @ 4.2.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 802 "hpack @ 4.1.0 (.) [dependency-of] h2 @ 4.2.0 (.)", 803 "httpcore @ 1.0.7 (.) [dependency-of] httpx @ 0.28.1 (.)", 804 "httpx @ 0.28.1 (.) [dependency-of] testpkg @ 0.1.0 (.)", 805 "hyperframe @ 6.1.0 (.) [dependency-of] h2 @ 4.2.0 (.)", 806 "idna @ 3.10 (.) [dependency-of] anyio @ 4.6.2.post1 (.)", 807 "idna @ 3.10 (.) [dependency-of] httpx @ 0.28.1 (.)", 808 "pprintpp @ 0.4.0 (.) [dependency-of] rich @ 0.3.3 (.)", 809 "pycparser @ 2.22 (.) [dependency-of] cffi @ 1.17.1 (.)", 810 "sniffio @ 1.3.1 (.) [dependency-of] anyio @ 4.6.2.post1 (.)", 811 "socksio @ 1.0.0 (.) [dependency-of] httpx @ 0.28.1 (.)", 812 "typing-extensions @ 3.10.0.2 (.) [dependency-of] rich @ 0.3.3 (.)", 813 // ideally we should NOT see these dependencies. However, they are technically installed in the environment 814 // and an import is present in httpx for each of these, so in theory they are actually dependencies even 815 // though our pyproject.toml looks like this: 816 // 817 // [tool.poetry.dependencies] 818 // python = "^3.11" 819 // httpx = {extras = ["brotli", "http2", "socks"], version = "^0.27.0"} 820 // pygments = "1.6" 821 // click = "<8" 822 // rich = "<10" 823 // 824 // note that pygments, click, and rich are installed outside of the allowable ranges for the given 825 // httpx package version constraints, per the poetry.lock: 826 // 827 // # for package httpx 828 // [package.extras] 829 // cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 830 // 831 // note: the pyproject.toml and uv.lock state are consistent with each other (just with 832 // "uv sync" and nothing was forced!) 833 "click @ 7.1.2 (.) [dependency-of] testpkg @ 0.1.0 (.)", 834 "pygments @ 1.6 (.) [dependency-of] testpkg @ 0.1.0 (.)", 835 "rich @ 0.3.3 (.) [dependency-of] testpkg @ 0.1.0 (.)", 836 }, 837 }, 838 } 839 840 for _, test := range tests { 841 t.Run(test.name, func(t *testing.T) { 842 pkgtest.NewCatalogTester(). 843 FromDirectory(t, test.fixture). 844 WithPackageStringer(stringPackage). 845 ExpectsRelationshipStrings(test.expectedRelationships). 846 TestCataloger(t, NewPackageCataloger(DefaultCatalogerConfig())) 847 }) 848 } 849 } 850 851 func Test_PackageCataloger_SitePackageRelationships(t *testing.T) { 852 tests := []struct { 853 name string 854 fixture string 855 expectedRelationships []string 856 }{ 857 { 858 name: "with multiple python installations and virtual envs", 859 fixture: "image-multi-site-package", 860 expectedRelationships: []string{ 861 // purely python 3.9 dist-packages 862 // 863 // in the container, you can get a sense for dependencies with : 864 // $ python3.9 -m pip list | tail -n +3 | awk '{print $1}' | xargs python3.9 -m pip show | grep -e 'Name:' -e 'Requires:' -e '\-\-\-' 865 // 866 // which approximates to (all in system packages): 867 // 868 // - beautifulsoup4: soupsieve 869 // - requests: certifi, chardet, idna, urllib3 870 // - blessed: six, wcwidth 871 // - virtualenv: distlib, filelock, platformdirs 872 "certifi @ 2020.12.5 (/usr/local/lib/python3.9/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.9/dist-packages)", 873 "certifi @ 2020.12.5 (/usr/local/lib/python3.9/dist-packages) [dependency-of] urllib3 @ 1.26.18 (/usr/local/lib/python3.9/dist-packages)", // available when extra == "secure", but another dependency is primarily installing it 874 "chardet @ 3.0.4 (/usr/local/lib/python3.9/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.9/dist-packages)", 875 "distlib @ 0.3.9 (/usr/local/lib/python3.9/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages)", 876 "filelock @ 3.18.0 (/usr/local/lib/python3.9/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages)", 877 "idna @ 2.10 (/usr/local/lib/python3.9/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.9/dist-packages)", 878 "idna @ 2.10 (/usr/local/lib/python3.9/dist-packages) [dependency-of] urllib3 @ 1.26.18 (/usr/local/lib/python3.9/dist-packages)", // available when extra == "secure", but another dependency is primarily installing it 879 "platformdirs @ 4.3.8 (/usr/local/lib/python3.9/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages)", 880 "six @ 1.16.0 (/usr/local/lib/python3.9/dist-packages) [dependency-of] blessed @ 1.20.0 (/usr/local/lib/python3.9/dist-packages)", 881 "soupsieve @ 2.2.1 (/usr/local/lib/python3.9/dist-packages) [dependency-of] beautifulsoup4 @ 4.9.3 (/usr/local/lib/python3.9/dist-packages)", 882 "urllib3 @ 1.26.18 (/usr/local/lib/python3.9/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.9/dist-packages)", 883 "virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages) [dependency-of] filelock @ 3.18.0 (/usr/local/lib/python3.9/dist-packages)", // available when extra == "testing", but we are installing it 884 "wcwidth @ 0.2.13 (/usr/local/lib/python3.9/dist-packages) [dependency-of] blessed @ 1.20.0 (/usr/local/lib/python3.9/dist-packages)", 885 886 // purely python 3.8 dist-packages 887 // 888 // in the container, you can get a sense for dependencies with : 889 // $ python3.8 -m pip list | tail -n +3 | awk '{print $1}' | xargs python3.8 -m pip show | grep -e 'Name:' -e 'Requires:' -e '\-\-\-' 890 // 891 // which approximates to (all in system packages): 892 // 893 // - beautifulsoup4: soupsieve 894 // - requests: certifi, chardet, idna, urllib3 895 // - runs: xmod 896 // - virtualenv: distlib, filelock, platformdirs 897 "certifi @ 2020.12.5 (/usr/local/lib/python3.8/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.8/dist-packages)", 898 "certifi @ 2020.12.5 (/usr/local/lib/python3.8/dist-packages) [dependency-of] urllib3 @ 1.26.18 (/usr/local/lib/python3.8/dist-packages)", // available when extra == "secure", but another dependency is primarily installing it 899 "chardet @ 3.0.4 (/usr/local/lib/python3.8/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.8/dist-packages)", 900 "distlib @ 0.3.9 (/usr/local/lib/python3.8/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.8/dist-packages)", 901 "filelock @ 3.16.1 (/usr/local/lib/python3.8/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.8/dist-packages)", 902 "idna @ 2.10 (/usr/local/lib/python3.8/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.8/dist-packages)", 903 "idna @ 2.10 (/usr/local/lib/python3.8/dist-packages) [dependency-of] urllib3 @ 1.26.18 (/usr/local/lib/python3.8/dist-packages)", // available when extra == "secure", but another dependency is primarily installing it 904 "platformdirs @ 4.3.6 (/usr/local/lib/python3.8/dist-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.8/dist-packages)", 905 "soupsieve @ 2.2 (/usr/local/lib/python3.8/dist-packages) [dependency-of] beautifulsoup4 @ 4.9.2 (/usr/local/lib/python3.8/dist-packages)", 906 "urllib3 @ 1.26.18 (/usr/local/lib/python3.8/dist-packages) [dependency-of] requests @ 2.25.0 (/usr/local/lib/python3.8/dist-packages)", 907 "virtualenv @ 20.31.2 (/usr/local/lib/python3.8/dist-packages) [dependency-of] filelock @ 3.16.1 (/usr/local/lib/python3.8/dist-packages)", // available when extra == "testing", but we are installing it 908 "xmod @ 1.8.1 (/usr/local/lib/python3.8/dist-packages) [dependency-of] runs @ 1.2.2 (/usr/local/lib/python3.8/dist-packages)", 909 910 // project 1 virtual env 911 // 912 // in the container, you can get a sense for dependencies with : 913 // $ source /app/project1/venv/bin/activate 914 // $ pip list | tail -n +3 | awk '{print $1}' | xargs pip show | grep -e 'Name:' -e 'Requires:' -e '\-\-\-' -e 'Location:' | grep -A 1 -B 1 '\-packages' 915 // 916 // which approximates to (some in virtual env, some in system packages): 917 // 918 // - beautifulsoup4: soupsieve 919 // - requests [SYSTEM]: certifi [SYSTEM], chardet [SYSTEM], idna [SYSTEM], urllib3 [SYSTEM] 920 // - blessed [SYSTEM]: six [SYSTEM], wcwidth [SYSTEM] 921 // - virtualenv [SYSTEM]: distlib [SYSTEM], filelock [SYSTEM], platformdirs [SYSTEM] 922 // - inquirer: python-editor [SYSTEM], blessed [SYSTEM], readchar 923 // 924 // Note: we'll only see new relationships, so any relationship where there is at least one new player (in FROM or TO) 925 "blessed @ 1.20.0 (/usr/local/lib/python3.9/dist-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)", // note: depends on global site package! 926 "python-editor @ 1.0.4 (/usr/local/lib/python3.9/dist-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)", // note: depends on global site package! 927 "readchar @ 4.2.1 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] inquirer @ 3.0.0 (/app/project1/venv/lib/python3.9/site-packages)", 928 "setuptools @ 44.0.0 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] virtualenv @ 20.31.2 (/usr/local/lib/python3.9/dist-packages)", // available when extra == "test", but we are installing it 929 "soupsieve @ 2.3 (/app/project1/venv/lib/python3.9/site-packages) [dependency-of] beautifulsoup4 @ 4.10.0 (/app/project1/venv/lib/python3.9/site-packages)", 930 931 // project 2 virtual env 932 // 933 // in the container, you can get a sense for dependencies with : 934 // $ source /app/project2/venv/bin/activate 935 // $ pip list | tail -n +3 | awk '{print $1}' | xargs pip show | grep -e 'Name:' -e 'Requires:' -e '\-\-\-' -e 'Location:' 936 // 937 // which approximates to (all in virtual env): 938 // 939 // - blessed: six, wcwidth 940 // - editor: runs, xmod 941 // - runs: xmod 942 // - inquirer: editor, blessed, readchar 943 "blessed @ 1.20.0 (/app/project2/venv/lib/python3.8/site-packages) [dependency-of] inquirer @ 3.2.4 (/app/project2/venv/lib/python3.8/site-packages)", 944 "editor @ 1.6.6 (/app/project2/venv/lib/python3.8/site-packages) [dependency-of] inquirer @ 3.2.4 (/app/project2/venv/lib/python3.8/site-packages)", 945 "readchar @ 4.1.0 (/app/project2/venv/lib/python3.8/site-packages) [dependency-of] inquirer @ 3.2.4 (/app/project2/venv/lib/python3.8/site-packages)", 946 "runs @ 1.2.2 (/app/project2/venv/lib/python3.8/site-packages) [dependency-of] editor @ 1.6.6 (/app/project2/venv/lib/python3.8/site-packages)", 947 "six @ 1.16.0 (/app/project2/venv/lib/python3.8/site-packages) [dependency-of] blessed @ 1.20.0 (/app/project2/venv/lib/python3.8/site-packages)", 948 "wcwidth @ 0.2.13 (/app/project2/venv/lib/python3.8/site-packages) [dependency-of] blessed @ 1.20.0 (/app/project2/venv/lib/python3.8/site-packages)", 949 "xmod @ 1.8.1 (/app/project2/venv/lib/python3.8/site-packages) [dependency-of] editor @ 1.6.6 (/app/project2/venv/lib/python3.8/site-packages)", 950 "xmod @ 1.8.1 (/app/project2/venv/lib/python3.8/site-packages) [dependency-of] runs @ 1.2.2 (/app/project2/venv/lib/python3.8/site-packages)", 951 }, 952 }, 953 } 954 955 for _, test := range tests { 956 t.Run(test.name, func(t *testing.T) { 957 pkgtest.NewCatalogTester(). 958 WithImageResolver(t, test.fixture). 959 WithPackageStringer(stringPackage). 960 ExpectsRelationshipStrings(test.expectedRelationships). 961 TestCataloger(t, NewInstalledPackageCataloger()) 962 }) 963 } 964 } 965 966 func stringPackage(p pkg.Package) string { 967 locs := p.Locations.ToSlice() 968 var loc string 969 if len(locs) > 0 { 970 // we want the location of the site-packages, not the metadata file 971 loc = path.Dir(path.Dir(p.Locations.ToSlice()[0].RealPath)) 972 } 973 974 return fmt.Sprintf("%s @ %s (%s)", p.Name, p.Version, loc) 975 } 976 977 func mustContentsFromLocation(t *testing.T, contentsPath string, offset ...int) string { 978 t.Helper() // Marks this function as a test helper for cleaner error reporting 979 contents, err := os.ReadFile(contentsPath) 980 if err != nil { 981 t.Fatalf("failed to read file %s: %v", contentsPath, err) 982 } 983 984 if len(offset) == 0 { 985 return string(contents) 986 } 987 988 if len(offset) != 2 { 989 t.Fatalf("invalid offset provided, expected two integers: start and end") 990 } 991 start, end := offset[0], offset[1] 992 993 if start < 0 || end > len(contents) || start > end { 994 t.Fatalf("invalid offset range: start=%d, end=%d, content length=%d", start, end, len(contents)) 995 } 996 997 return string(contents[start:end]) 998 }