Expand python license scanning to cover unclaimed files (#3779)

* expand python license scanning to cover unclaimed files

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

* speed up tests using the license scanner

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>

---------

Signed-off-by: Alex Goodman <wagoodman@users.noreply.github.com>
This commit is contained in:
Alex Goodman 2025-04-03 10:31:02 -04:00 committed by GitHub
parent da62a82413
commit f851085668
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 730 additions and 303 deletions

View File

@ -7,15 +7,18 @@ import (
"os" "os"
"sort" "sort"
"strings" "strings"
"sync"
"testing" "testing"
"github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts" "github.com/google/go-cmp/cmp/cmpopts"
"github.com/google/licensecheck"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/anchore/stereoscope/pkg/imagetest" "github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/internal/cmptest" "github.com/anchore/syft/internal/cmptest"
"github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/relationship" "github.com/anchore/syft/internal/relationship"
"github.com/anchore/syft/syft/artifact" "github.com/anchore/syft/syft/artifact"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
@ -27,6 +30,11 @@ import (
"github.com/anchore/syft/syft/source/stereoscopesource" "github.com/anchore/syft/syft/source/stereoscopesource"
) )
var (
once sync.Once
licenseScanner *licenses.Scanner
)
type CatalogTester struct { type CatalogTester struct {
expectedPkgs []pkg.Package expectedPkgs []pkg.Package
expectedRelationships []artifact.Relationship expectedRelationships []artifact.Relationship
@ -44,10 +52,26 @@ type CatalogTester struct {
licenseComparer cmptest.LicenseComparer licenseComparer cmptest.LicenseComparer
packageStringer func(pkg.Package) string packageStringer func(pkg.Package) string
customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) customAssertions []func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship)
context context.Context
}
func Context() context.Context {
once.Do(func() {
// most of the time in testing is initializing the scanner. Let's do that just once
sc := &licenses.ScannerConfig{Scanner: licensecheck.Scan, CoverageThreshold: 75}
scanner, err := licenses.NewScanner(sc)
if err != nil {
panic("unable to setup licences scanner for testing")
}
licenseScanner = &scanner
})
return licenses.SetContextLicenseScanner(context.Background(), *licenseScanner)
} }
func NewCatalogTester() *CatalogTester { func NewCatalogTester() *CatalogTester {
return &CatalogTester{ return &CatalogTester{
context: Context(),
locationComparer: cmptest.DefaultLocationComparer, locationComparer: cmptest.DefaultLocationComparer,
licenseComparer: cmptest.DefaultLicenseComparer, licenseComparer: cmptest.DefaultLicenseComparer,
packageStringer: stringPackage, packageStringer: stringPackage,
@ -64,6 +88,11 @@ func NewCatalogTester() *CatalogTester {
} }
} }
func (p *CatalogTester) WithContext(ctx context.Context) *CatalogTester {
p.context = ctx
return p
}
func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester { func (p *CatalogTester) FromDirectory(t *testing.T, path string) *CatalogTester {
t.Helper() t.Helper()
@ -202,7 +231,7 @@ func (p *CatalogTester) IgnoreUnfulfilledPathResponses(paths ...string) *Catalog
func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) { func (p *CatalogTester) TestParser(t *testing.T, parser generic.Parser) {
t.Helper() t.Helper()
pkgs, relationships, err := parser(context.Background(), p.resolver, p.env, p.reader) pkgs, relationships, err := parser(p.context, p.resolver, p.env, p.reader)
// only test for errors if explicitly requested // only test for errors if explicitly requested
if p.wantErr != nil { if p.wantErr != nil {
p.wantErr(t, err) p.wantErr(t, err)
@ -215,7 +244,7 @@ func (p *CatalogTester) TestCataloger(t *testing.T, cataloger pkg.Cataloger) {
resolver := NewObservingResolver(p.resolver) resolver := NewObservingResolver(p.resolver)
pkgs, relationships, err := cataloger.Catalog(context.Background(), resolver) pkgs, relationships, err := cataloger.Catalog(p.context, resolver)
// this is a minimum set, the resolver may return more that just this list // this is a minimum set, the resolver may return more that just this list
for _, path := range p.expectedPathResponses { for _, path := range p.expectedPathResponses {

View File

@ -1,7 +1,6 @@
package python package python
import ( import (
"context"
"fmt" "fmt"
"path" "path"
"testing" "testing"
@ -16,116 +15,47 @@ import (
func Test_PackageCataloger(t *testing.T) { func Test_PackageCataloger(t *testing.T) {
tests := []struct { tests := []struct {
name string name string
fixtures []string fixture string
expectedPackage pkg.Package expectedPackages []pkg.Package
}{ }{
{ {
name: "egg-file-no-version", name: "egg-file-no-version",
fixtures: []string{"test-fixtures/no-version-py3.8.egg-info"}, fixture: "test-fixtures/site-packages/no-version",
expectedPackage: pkg.Package{ expectedPackages: []pkg.Package{
{
Name: "no-version", Name: "no-version",
Locations: file.NewLocationSet(file.NewLocation("no-version-py3.8.egg-info")),
PURL: "pkg:pypi/no-version", PURL: "pkg:pypi/no-version",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
FoundBy: "python-installed-package-cataloger", FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{ Metadata: pkg.PythonPackage{
Name: "no-version", Name: "no-version",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: ".", // requires scanning the grandparent directory to get a valid path
},
}, },
}, },
}, },
{ {
name: "egg-info directory", name: "dist-info+egg-info site-packages directory",
fixtures: []string{ fixture: "test-fixtures/site-packages/nested",
"test-fixtures/egg-info/PKG-INFO", expectedPackages: []pkg.Package{
"test-fixtures/egg-info/RECORD",
"test-fixtures/egg-info/top_level.txt",
},
expectedPackage: pkg.Package{
Name: "requests",
Version: "2.22.0",
PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("test-fixtures/egg-info/PKG-INFO")),
),
FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{
Name: "requests",
Version: "2.22.0",
Platform: "UNKNOWN",
Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "test-fixtures",
Files: []pkg.PythonFileRecord{
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
{Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
{Path: "requests/__pycache__/__version__.cpython-38.pyc"},
{Path: "requests/__pycache__/utils.cpython-38.pyc"},
{Path: "requests/__version__.py", Digest: &pkg.PythonFileDigest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
{Path: "requests/utils.py", Digest: &pkg.PythonFileDigest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
},
TopLevelPackages: []string{"requests"},
RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
ProvidesExtra: []string{"security", "socks"},
},
},
},
{ {
name: "egg-info directory case sensitive",
fixtures: []string{
"test-fixtures/casesensitive/EGG-INFO/PKG-INFO",
"test-fixtures/casesensitive/EGG-INFO/RECORD",
"test-fixtures/casesensitive/EGG-INFO/top_level.txt",
},
expectedPackage: pkg.Package{
Name: "requests",
Version: "2.22.0",
PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg,
Language: pkg.Python,
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("test-fixtures/casesensitive/EGG-INFO/PKG-INFO")),
),
FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{
Name: "requests",
Version: "2.22.0",
Platform: "UNKNOWN",
Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "test-fixtures/casesensitive",
Files: []pkg.PythonFileRecord{
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
{Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
{Path: "requests/__pycache__/__version__.cpython-38.pyc"},
{Path: "requests/__pycache__/utils.cpython-38.pyc"},
{Path: "requests/__version__.py", Digest: &pkg.PythonFileDigest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
{Path: "requests/utils.py", Digest: &pkg.PythonFileDigest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
},
TopLevelPackages: []string{"requests"},
RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
ProvidesExtra: []string{"security", "socks"},
},
},
},
{
name: "dist-info directory",
fixtures: []string{
"test-fixtures/dist-info/METADATA",
"test-fixtures/dist-info/RECORD",
"test-fixtures/dist-info/top_level.txt",
"test-fixtures/dist-info/direct_url.json",
},
expectedPackage: pkg.Package{
Name: "pygments", Name: "pygments",
Version: "2.6.1", Version: "2.6.1",
PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("dist-name/dist-info/METADATA"),
file.NewLocation("dist-name/dist-info/RECORD"),
file.NewLocation("dist-name/dist-info/direct_url.json"),
file.NewLocation("dist-name/dist-info/top_level.txt"),
),
Licenses: pkg.NewLicenseSet( Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", file.NewLocation("test-fixtures/dist-info/METADATA")), // here we only used the license that was declared in the METADATA file, we did not go searching for other licenses
// this is the better source of truth when there is no explicit LicenseFile given
pkg.NewLicenseFromLocations("BSD License", file.NewLocation("dist-name/dist-info/METADATA")),
), ),
FoundBy: "python-installed-package-cataloger", FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{ Metadata: pkg.PythonPackage{
@ -134,10 +64,11 @@ func Test_PackageCataloger(t *testing.T) {
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: "dist-name",
Files: []pkg.PythonFileRecord{ Files: []pkg.PythonFileRecord{
{Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"}, {Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"}, {Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "Pygments-2.6.1.dist-info/LICENSE.txt", Digest: &pkg.PythonFileDigest{Algorithm: "sha256", Value: "utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "Pygments-2.6.1.dist-info/RECORD"}, {Path: "Pygments-2.6.1.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"}, {Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"}, {Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
@ -151,23 +82,63 @@ func Test_PackageCataloger(t *testing.T) {
ProvidesExtra: []string{"html5lib", "lxml"}, ProvidesExtra: []string{"html5lib", "lxml"},
}, },
}, },
{
Name: "requests",
Version: "2.22.0",
PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg,
Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("egg-name/egg-info/PKG-INFO"),
file.NewLocation("egg-name/egg-info/RECORD"),
file.NewLocation("egg-name/egg-info/top_level.txt"),
),
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("egg-name/egg-info/PKG-INFO")),
),
FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{
Name: "requests",
Version: "2.22.0",
Platform: "UNKNOWN",
Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "egg-name",
Files: []pkg.PythonFileRecord{
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
{Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
{Path: "requests/__pycache__/__version__.cpython-38.pyc"},
{Path: "requests/__pycache__/utils.cpython-38.pyc"},
{Path: "requests/__version__.py", Digest: &pkg.PythonFileDigest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
{Path: "requests/utils.py", Digest: &pkg.PythonFileDigest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
},
TopLevelPackages: []string{"requests"},
RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
ProvidesExtra: []string{"security", "socks"},
},
},
},
}, },
{ {
name: "dist-info directory case sensitive", name: "DIST-INFO+EGG-INFO site-packages directory (case insensitive)",
fixtures: []string{ fixture: "test-fixtures/site-packages/uppercase",
"test-fixtures/casesensitive/DIST-INFO/METADATA", expectedPackages: []pkg.Package{
"test-fixtures/casesensitive/DIST-INFO/RECORD", {
"test-fixtures/casesensitive/DIST-INFO/top_level.txt",
"test-fixtures/casesensitive/DIST-INFO/direct_url.json",
},
expectedPackage: pkg.Package{
Name: "pygments", Name: "pygments",
Version: "2.6.1", Version: "2.6.1",
PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("dist-name/DIST-INFO/METADATA"),
file.NewLocation("dist-name/DIST-INFO/RECORD"),
file.NewLocation("dist-name/DIST-INFO/direct_url.json"),
file.NewLocation("dist-name/DIST-INFO/top_level.txt"),
),
Licenses: pkg.NewLicenseSet( Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", file.NewLocation("test-fixtures/casesensitive/DIST-INFO/METADATA")), // here we only used the license that was declared in the METADATA file, we did not go searching for other licenses
// this is the better source of truth when there is no explicit LicenseFile given
pkg.NewLicenseFromLocations("BSD License", file.NewLocation("dist-name/DIST-INFO/METADATA")),
), ),
FoundBy: "python-installed-package-cataloger", FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{ Metadata: pkg.PythonPackage{
@ -176,7 +147,7 @@ func Test_PackageCataloger(t *testing.T) {
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures/casesensitive", SitePackagesRootPath: "dist-name",
Files: []pkg.PythonFileRecord{ Files: []pkg.PythonFileRecord{
{Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"}, {Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"}, {Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
@ -191,21 +162,67 @@ func Test_PackageCataloger(t *testing.T) {
RequiresPython: ">=3.5", RequiresPython: ">=3.5",
}, },
}, },
},
{ {
name: "malformed-record", Name: "requests",
fixtures: []string{ Version: "2.22.0",
"test-fixtures/malformed-record/dist-info/METADATA", PURL: "pkg:pypi/requests@2.22.0",
"test-fixtures/malformed-record/dist-info/RECORD",
},
expectedPackage: pkg.Package{
Name: "pygments",
Version: "2.6.1",
PURL: "pkg:pypi/pygments@2.6.1",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("egg-name/EGG-INFO/PKG-INFO"),
file.NewLocation("egg-name/EGG-INFO/RECORD"),
file.NewLocation("egg-name/EGG-INFO/top_level.txt"),
),
Licenses: pkg.NewLicenseSet( Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", file.NewLocation("test-fixtures/malformed-record/dist-info/METADATA")), pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("egg-name/EGG-INFO/PKG-INFO")),
),
FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{
Name: "requests",
Version: "2.22.0",
Platform: "UNKNOWN",
Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "egg-name",
Files: []pkg.PythonFileRecord{
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
{Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
{Path: "requests/__pycache__/__version__.cpython-38.pyc"},
{Path: "requests/__pycache__/utils.cpython-38.pyc"},
{Path: "requests/__version__.py", Digest: &pkg.PythonFileDigest{"sha256", "Bm-GFstQaFezsFlnmEMrJDe8JNROz9n2XXYtODdvjjc"}, Size: "436"},
{Path: "requests/utils.py", Digest: &pkg.PythonFileDigest{"sha256", "LtPJ1db6mJff2TJSJWKi7rBpzjPS3mSOrjC9zRhoD3A"}, Size: "30049"},
},
TopLevelPackages: []string{"requests"},
RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
ProvidesExtra: []string{"security", "socks"},
},
},
},
},
{
name: "detect licenses",
fixture: "test-fixtures/site-packages/license",
expectedPackages: []pkg.Package{
{
Name: "pygments",
Version: "2.6.1",
PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Type: pkg.PythonPkg,
Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("with-license-file-declared.dist-info/METADATA"), // the LicenseFile is declared in the METADATA file
file.NewLocation("with-license-file-declared.dist-info/RECORD"),
file.NewLocation("with-license-file-declared.dist-info/top_level.txt"),
file.NewLocation("with-license-file-declared.dist-info/direct_url.json"),
),
Licenses: pkg.NewLicenseSet(
pkg.License{
Value: "BSD-3-Clause",
SPDXExpression: "BSD-3-Clause",
Type: "concluded",
// we read the path from the LicenseFile field in the METADATA file, then read the license file directly
Locations: file.NewLocationSet(file.NewLocation("with-license-file-declared.dist-info/LICENSE.txt")),
},
), ),
FoundBy: "python-installed-package-cataloger", FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{ Metadata: pkg.PythonPackage{
@ -214,7 +231,97 @@ func Test_PackageCataloger(t *testing.T) {
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures/malformed-record", SitePackagesRootPath: ".",
Files: []pkg.PythonFileRecord{
{Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
{Path: "with-license-file-declared.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "with-license-file-declared.dist-info/LICENSE.txt", Digest: &pkg.PythonFileDigest{Algorithm: "sha256", Value: "utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "with-license-file-declared.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
{Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"},
},
TopLevelPackages: []string{"pygments", "something_else"},
DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{URL: "https://github.com/python-test/test.git", VCS: "git", CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
RequiresPython: ">=3.5",
RequiresDist: []string{"soupsieve (>1.2)", "html5lib ; extra == 'html5lib'", "lxml ; extra == 'lxml'"},
ProvidesExtra: []string{"html5lib", "lxml"},
},
},
{
Name: "pygments",
Version: "2.6.1",
PURL: "pkg:pypi/pygments@2.6.1?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fpython-test%2Ftest.git%40aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
Type: pkg.PythonPkg,
Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("without-license-file-declared.dist-info/METADATA"), // the LicenseFile is declared in the METADATA file
file.NewLocation("without-license-file-declared.dist-info/RECORD"),
file.NewLocation("without-license-file-declared.dist-info/top_level.txt"),
file.NewLocation("without-license-file-declared.dist-info/direct_url.json"),
),
Licenses: pkg.NewLicenseSet(
pkg.License{
Value: "BSD-3-Clause",
SPDXExpression: "BSD-3-Clause",
Type: "concluded",
// we discover license files automatically
Locations: file.NewLocationSet(file.NewLocation("without-license-file-declared.dist-info/LICENSE.txt")),
},
),
FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{
Name: "Pygments",
Version: "2.6.1",
Platform: "any",
Author: "Georg Brandl",
AuthorEmail: "georg@python.org",
SitePackagesRootPath: ".",
Files: []pkg.PythonFileRecord{
{Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
{Path: "without-license-file-declared.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "without-license-file-declared.dist-info/LICENSE.txt", Digest: &pkg.PythonFileDigest{Algorithm: "sha256", Value: "utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "without-license-file-declared.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},
{Path: "pygments/x_util.py", Digest: &pkg.PythonFileDigest{"sha256", "qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI="}, Size: "10778"},
},
TopLevelPackages: []string{"pygments", "something_else"},
DirectURLOrigin: &pkg.PythonDirectURLOriginInfo{URL: "https://github.com/python-test/test.git", VCS: "git", CommitID: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"},
RequiresPython: ">=3.5",
RequiresDist: []string{"soupsieve (>1.2)", "html5lib ; extra == 'html5lib'", "lxml ; extra == 'lxml'"},
ProvidesExtra: []string{"html5lib", "lxml"},
},
},
},
},
{
name: "malformed-record",
fixture: "test-fixtures/site-packages/malformed-record",
expectedPackages: []pkg.Package{
{
Name: "pygments",
Version: "2.6.1",
PURL: "pkg:pypi/pygments@2.6.1",
Type: pkg.PythonPkg,
Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("dist-info/METADATA"),
file.NewLocation("dist-info/RECORD"),
),
Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", file.NewLocation("dist-info/METADATA")),
),
FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{
Name: "Pygments",
Version: "2.6.1",
Platform: "any",
Author: "Georg Brandl",
AuthorEmail: "georg@python.org",
SitePackagesRootPath: ".",
Files: []pkg.PythonFileRecord{ Files: []pkg.PythonFileRecord{
{Path: "flask/json/tag.py", Digest: &pkg.PythonFileDigest{"sha256", "9ehzrmt5k7hxf7ZEK0NOs3swvQyU9fWNe-pnYe69N60"}, Size: "8223"}, {Path: "flask/json/tag.py", Digest: &pkg.PythonFileDigest{"sha256", "9ehzrmt5k7hxf7ZEK0NOs3swvQyU9fWNe-pnYe69N60"}, Size: "8223"},
{Path: "../../Scripts/flask.exe", Digest: &pkg.PythonFileDigest{"sha256", "mPrbVeZCDX20himZ_bRai1nCs_tgr7jHIOGZlcgn-T4"}, Size: "93063"}, {Path: "../../Scripts/flask.exe", Digest: &pkg.PythonFileDigest{"sha256", "mPrbVeZCDX20himZ_bRai1nCs_tgr7jHIOGZlcgn-T4"}, Size: "93063"},
@ -225,19 +332,24 @@ func Test_PackageCataloger(t *testing.T) {
}, },
}, },
}, },
},
{ {
// in cases where the metadata file is available and the record is not we should still record there is a package // in cases where the metadata file is available and the record is not we should still record there is a package
// additionally empty top_level.txt files should not result in an error // additionally empty top_level.txt files should not result in an error
name: "partial dist-info directory", name: "partial dist-info directory",
fixtures: []string{"test-fixtures/partial.dist-info/METADATA"}, fixture: "test-fixtures/site-packages/partial.dist-info",
expectedPackage: pkg.Package{ expectedPackages: []pkg.Package{
{
Name: "pygments", Name: "pygments",
Version: "2.6.1", Version: "2.6.1",
PURL: "pkg:pypi/pygments@2.6.1", PURL: "pkg:pypi/pygments@2.6.1",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("METADATA"),
),
Licenses: pkg.NewLicenseSet( Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("BSD License", file.NewLocation("test-fixtures/partial.dist-info/METADATA")), pkg.NewLicenseFromLocations("BSD License", file.NewLocation("METADATA")),
), ),
FoundBy: "python-installed-package-cataloger", FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{ Metadata: pkg.PythonPackage{
@ -246,22 +358,27 @@ func Test_PackageCataloger(t *testing.T) {
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: ".",
RequiresPython: ">=3.5", RequiresPython: ">=3.5",
}, },
}, },
}, },
},
{ {
name: "egg-info regular file", name: "egg-info regular file",
fixtures: []string{"test-fixtures/test.egg-info"}, fixture: "test-fixtures/site-packages/test",
expectedPackage: pkg.Package{ expectedPackages: []pkg.Package{
{
Name: "requests", Name: "requests",
Version: "2.22.0", Version: "2.22.0",
PURL: "pkg:pypi/requests@2.22.0", PURL: "pkg:pypi/requests@2.22.0",
Type: pkg.PythonPkg, Type: pkg.PythonPkg,
Language: pkg.Python, Language: pkg.Python,
Locations: file.NewLocationSet(
file.NewLocation("test.egg-info"),
),
Licenses: pkg.NewLicenseSet( Licenses: pkg.NewLicenseSet(
pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("test-fixtures/test.egg-info")), pkg.NewLicenseFromLocations("Apache 2.0", file.NewLocation("test.egg-info")),
), ),
FoundBy: "python-installed-package-cataloger", FoundBy: "python-installed-package-cataloger",
Metadata: pkg.PythonPackage{ Metadata: pkg.PythonPackage{
@ -270,26 +387,20 @@ func Test_PackageCataloger(t *testing.T) {
Platform: "UNKNOWN", Platform: "UNKNOWN",
Author: "Kenneth Reitz", Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org", AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: ".",
RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
ProvidesExtra: []string{"security", "socks"}, ProvidesExtra: []string{"security", "socks"},
}, },
}, },
}, },
},
} }
for _, test := range tests { for _, test := range tests {
t.Run(test.name, func(t *testing.T) { t.Run(test.name, func(t *testing.T) {
resolver := file.NewMockResolverForPaths(test.fixtures...)
locations, err := resolver.FilesByPath(test.fixtures...)
require.NoError(t, err)
test.expectedPackage.Locations = file.NewLocationSet(locations...)
pkgtest.NewCatalogTester(). pkgtest.NewCatalogTester().
WithResolver(resolver). FromDirectory(t, test.fixture).
Expects([]pkg.Package{test.expectedPackage}, nil). Expects(test.expectedPackages, nil).
TestCataloger(t, NewInstalledPackageCataloger()) TestCataloger(t, NewInstalledPackageCataloger())
}) })
} }
@ -311,7 +422,7 @@ func Test_PackageCataloger_IgnorePackage(t *testing.T) {
t.Run(test.MetadataFixture, func(t *testing.T) { t.Run(test.MetadataFixture, func(t *testing.T) {
resolver := file.NewMockResolverForPaths(test.MetadataFixture) resolver := file.NewMockResolverForPaths(test.MetadataFixture)
actual, _, err := NewInstalledPackageCataloger().Catalog(context.Background(), resolver) actual, _, err := NewInstalledPackageCataloger().Catalog(pkgtest.Context(), resolver)
require.NoError(t, err) require.NoError(t, err)
if len(actual) != 0 { if len(actual) != 0 {

View File

@ -1,14 +1,11 @@
package python package python
import ( import (
"context"
"fmt" "fmt"
"regexp" "regexp"
"strings" "strings"
"github.com/anchore/packageurl-go" "github.com/anchore/packageurl-go"
"github.com/anchore/syft/internal/licenses"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/pkg"
) )
@ -92,38 +89,6 @@ func newPackageForPackage(m parsedData, licenses pkg.LicenseSet, sources ...file
return p return p
} }
func findLicenses(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, m parsedData) pkg.LicenseSet {
var licenseSet pkg.LicenseSet
switch {
case m.LicenseExpression != "":
licenseSet = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(m.LicenseLocation, m.LicenseExpression)...)
case m.Licenses != "":
licenseSet = pkg.NewLicenseSet(pkg.NewLicensesFromLocation(m.LicenseLocation, m.Licenses)...)
case m.LicenseLocation.Path() != "":
// If we have a license file then resolve and parse it
found, err := resolver.FilesByPath(m.LicenseLocation.Path())
if err != nil {
log.WithFields("error", err, "path", m.LicenseLocation.Path()).Trace("unable to resolve python license")
}
if len(found) > 0 {
metadataContents, err := resolver.FileContentsByLocation(found[0])
if err == nil {
parsed, err := scanner.PkgSearch(ctx, file.NewLocationReadCloser(m.LicenseLocation, metadataContents))
if err != nil {
log.WithFields("error", err, "path", m.LicenseLocation.Path()).Trace("unable to parse a license from the file")
}
if len(parsed) > 0 {
licenseSet = pkg.NewLicenseSet(parsed...)
}
} else {
log.WithFields("error", err, "path", m.LicenseLocation.Path()).Trace("unable to read file contents")
}
}
}
return licenseSet
}
func packageURL(name, version string, m *pkg.PythonPackage) string { func packageURL(name, version string, m *pkg.PythonPackage) string {
// generate a purl from the package data // generate a purl from the package data
pURL := packageurl.NewPackageURL( pURL := packageurl.NewPackageURL(

View File

@ -6,7 +6,12 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io" "io"
"path"
"path/filepath" "path/filepath"
"sort"
"strings"
"github.com/scylladb/go-set/strset"
"github.com/anchore/syft/internal" "github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/licenses" "github.com/anchore/syft/internal/licenses"
@ -247,3 +252,97 @@ func assembleEggOrWheelMetadata(resolver file.Resolver, metadataLocation file.Lo
pd.DirectURLOrigin = d pd.DirectURLOrigin = d
return &pd, sources, nil return &pd, sources, nil
} }
func findLicenses(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, m parsedData) pkg.LicenseSet {
var licenseSet pkg.LicenseSet
licenseLocations := file.NewLocationSet()
if m.LicenseFilePath != "" {
locs, err := resolver.FilesByPath(m.LicenseFilePath)
if err != nil {
log.WithFields("error", err, "path", m.LicenseFilePath).Trace("unable to resolve python license file")
} else {
licenseLocations.Add(locs...)
}
}
switch {
case m.LicenseExpression != "" || m.Licenses != "":
licenseSet = getLicenseSetFromValues(licenseLocations.ToSlice(), m.LicenseExpression, m.Licenses)
case !licenseLocations.Empty():
licenseSet = getLicenseSetFromFiles(ctx, scanner, resolver, licenseLocations.ToSlice()...)
default:
// search for known license paths from RECORDS file
licenseNames := strset.New()
for _, n := range licenses.FileNames() {
licenseNames.Add(strings.ToLower(n))
}
parent := path.Base(path.Dir(m.DistInfoLocation.Path()))
candidatePaths := strset.New()
for _, f := range m.Files {
if !strings.HasPrefix(f.Path, parent) || strings.Count(f.Path, "/") > 1 {
continue
}
if licenseNames.Has(strings.ToLower(filepath.Base(f.Path))) {
candidatePaths.Add(path.Join(m.SitePackagesRootPath, f.Path))
}
}
paths := candidatePaths.List()
sort.Strings(paths)
locationSet := file.NewLocationSet()
for _, p := range paths {
locs, err := resolver.FilesByPath(p)
if err != nil {
log.WithFields("error", err, "path", p).Trace("unable to resolve python license in dist-info")
continue
}
locationSet.Add(locs...)
}
licenseSet = getLicenseSetFromFiles(ctx, scanner, resolver, locationSet.ToSlice()...)
}
return licenseSet
}
func getLicenseSetFromValues(locations []file.Location, licenseValues ...string) pkg.LicenseSet {
if len(locations) == 0 {
return pkg.NewLicenseSet(pkg.NewLicensesFromValues(licenseValues...)...)
}
licenseSet := pkg.NewLicenseSet()
for _, value := range licenseValues {
if value == "" {
continue
}
licenseSet.Add(pkg.NewLicenseFromLocations(value, locations...))
}
return licenseSet
}
func getLicenseSetFromFiles(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, locations ...file.Location) pkg.LicenseSet {
licenseSet := pkg.NewLicenseSet()
for _, loc := range locations {
licenseSet.Add(getLicenseSetFromFile(ctx, scanner, resolver, loc)...)
}
return licenseSet
}
func getLicenseSetFromFile(ctx context.Context, scanner licenses.Scanner, resolver file.Resolver, location file.Location) []pkg.License {
metadataContents, err := resolver.FileContentsByLocation(location)
if err != nil {
log.WithFields("error", err, "path", location.Path()).Trace("unable to read file contents")
return nil
}
defer internal.CloseAndLogError(metadataContents, location.Path())
parsed, err := scanner.PkgSearch(ctx, file.NewLocationReadCloser(location, metadataContents))
if err != nil {
log.WithFields("error", err, "path", location.Path()).Trace("unable to parse a license from the file")
return nil
}
return parsed
}

View File

@ -15,11 +15,18 @@ import (
) )
type parsedData struct { type parsedData struct {
// core info
// DistInfoLocation is the location of the METADATA file within the .dist-info directory where we obtained the python package information
DistInfoLocation file.Location
pkg.PythonPackage `mapstructure:",squash"`
// license info
Licenses string `mapstructure:"License"` Licenses string `mapstructure:"License"`
LicenseFile string `mapstructure:"LicenseFile"` LicenseFile string `mapstructure:"LicenseFile"`
LicenseExpression string `mapstructure:"LicenseExpression"` LicenseExpression string `mapstructure:"LicenseExpression"`
LicenseLocation file.Location LicenseFilePath string
pkg.PythonPackage `mapstructure:",squash"`
} }
var pluralFields = map[string]bool{ var pluralFields = map[string]bool{
@ -45,11 +52,13 @@ func parseWheelOrEggMetadata(locationReader file.LocationReadCloser) (parsedData
pd.SitePackagesRootPath = determineSitePackagesRootPath(path) pd.SitePackagesRootPath = determineSitePackagesRootPath(path)
if pd.Licenses != "" || pd.LicenseExpression != "" { if pd.Licenses != "" || pd.LicenseExpression != "" {
pd.LicenseLocation = file.NewLocation(path) pd.LicenseFilePath = path
} else if pd.LicenseFile != "" { } else if pd.LicenseFile != "" {
pd.LicenseLocation = file.NewLocation(filepath.Join(filepath.Dir(path), pd.LicenseFile)) pd.LicenseFilePath = filepath.Join(filepath.Dir(path), pd.LicenseFile)
} }
pd.DistInfoLocation = locationReader.Location
return pd, nil return pd, nil
} }

View File

@ -22,38 +22,40 @@ func TestParseWheelEggMetadata(t *testing.T) {
ExpectedMetadata parsedData ExpectedMetadata parsedData
}{ }{
{ {
Fixture: "test-fixtures/egg-info/PKG-INFO", Fixture: "test-fixtures/site-packages/nested/egg-name/egg-info/PKG-INFO",
ExpectedMetadata: parsedData{ ExpectedMetadata: parsedData{
"Apache 2.0", DistInfoLocation: file.NewLocation("test-fixtures/site-packages/nested/egg-name/egg-info/PKG-INFO"),
"", Licenses: "Apache 2.0",
"", LicenseFile: "",
file.NewLocation("test-fixtures/egg-info/PKG-INFO"), LicenseExpression: "",
pkg.PythonPackage{ LicenseFilePath: "test-fixtures/site-packages/nested/egg-name/egg-info/PKG-INFO",
PythonPackage: pkg.PythonPackage{
Name: "requests", Name: "requests",
Version: "2.22.0", Version: "2.22.0",
Platform: "UNKNOWN", Platform: "UNKNOWN",
Author: "Kenneth Reitz", Author: "Kenneth Reitz",
AuthorEmail: "me@kennethreitz.org", AuthorEmail: "me@kennethreitz.org",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: "test-fixtures/site-packages/nested/egg-name",
RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*", RequiresPython: ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*",
ProvidesExtra: []string{"security", "socks"}, ProvidesExtra: []string{"security", "socks"},
}, },
}, },
}, },
{ {
Fixture: "test-fixtures/dist-info/METADATA", Fixture: "test-fixtures/site-packages/nested/dist-name/dist-info/METADATA",
ExpectedMetadata: parsedData{ ExpectedMetadata: parsedData{
"BSD License", DistInfoLocation: file.NewLocation("test-fixtures/site-packages/nested/dist-name/dist-info/METADATA"),
"", Licenses: "BSD License",
"", LicenseFile: "",
file.NewLocation("test-fixtures/dist-info/METADATA"), LicenseExpression: "",
pkg.PythonPackage{ LicenseFilePath: "test-fixtures/site-packages/nested/dist-name/dist-info/METADATA",
PythonPackage: pkg.PythonPackage{
Name: "Pygments", Name: "Pygments",
Version: "2.6.1", Version: "2.6.1",
Platform: "any", Platform: "any",
Author: "Georg Brandl", Author: "Georg Brandl",
AuthorEmail: "georg@python.org", AuthorEmail: "georg@python.org",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: "test-fixtures/site-packages/nested/dist-name",
RequiresPython: ">=3.5", RequiresPython: ">=3.5",
RequiresDist: []string{"soupsieve (>1.2)", "html5lib ; extra == 'html5lib'", "lxml ; extra == 'lxml'"}, RequiresDist: []string{"soupsieve (>1.2)", "html5lib ; extra == 'html5lib'", "lxml ; extra == 'lxml'"},
ProvidesExtra: []string{"html5lib", "lxml"}, ProvidesExtra: []string{"html5lib", "lxml"},
@ -149,16 +151,17 @@ func TestParseWheelEggMetadataInvalid(t *testing.T) {
ExpectedMetadata parsedData ExpectedMetadata parsedData
}{ }{
{ {
Fixture: "test-fixtures/egg-info/PKG-INFO-INVALID", Fixture: "test-fixtures/site-packages/nested/egg-name/egg-info/PKG-INFO-INVALID",
ExpectedMetadata: parsedData{ ExpectedMetadata: parsedData{
"", DistInfoLocation: file.NewLocation("test-fixtures/site-packages/nested/egg-name/egg-info/PKG-INFO-INVALID"),
"", Licenses: "",
"", LicenseExpression: "",
file.Location{}, LicenseFile: "",
pkg.PythonPackage{ LicenseFilePath: "",
PythonPackage: pkg.PythonPackage{
Name: "mxnet", Name: "mxnet",
Version: "1.8.0", Version: "1.8.0",
SitePackagesRootPath: "test-fixtures", SitePackagesRootPath: "test-fixtures/site-packages/nested/egg-name",
}, },
}, },
}, },

View File

@ -15,7 +15,7 @@ func TestParseWheelEggRecord(t *testing.T) {
ExpectedMetadata []pkg.PythonFileRecord ExpectedMetadata []pkg.PythonFileRecord
}{ }{
{ {
Fixture: "test-fixtures/egg-info/RECORD", Fixture: "test-fixtures/site-packages/nested/egg-name/egg-info/RECORD",
ExpectedMetadata: []pkg.PythonFileRecord{ ExpectedMetadata: []pkg.PythonFileRecord{
{Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"}, {Path: "requests-2.22.0.dist-info/INSTALLER", Digest: &pkg.PythonFileDigest{"sha256", "zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg"}, Size: "4"},
{Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"}, {Path: "requests/__init__.py", Digest: &pkg.PythonFileDigest{"sha256", "PnKCgjcTq44LaAMzB-7--B2FdewRrE8F_vjZeaG9NhA"}, Size: "3921"},
@ -26,10 +26,11 @@ func TestParseWheelEggRecord(t *testing.T) {
}, },
}, },
{ {
Fixture: "test-fixtures/dist-info/RECORD", Fixture: "test-fixtures/site-packages/nested/dist-name/dist-info/RECORD",
ExpectedMetadata: []pkg.PythonFileRecord{ ExpectedMetadata: []pkg.PythonFileRecord{
{Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"}, {Path: "../../../bin/pygmentize", Digest: &pkg.PythonFileDigest{"sha256", "dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8"}, Size: "220"},
{Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"}, {Path: "Pygments-2.6.1.dist-info/AUTHORS", Digest: &pkg.PythonFileDigest{"sha256", "PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "Pygments-2.6.1.dist-info/LICENSE.txt", Digest: &pkg.PythonFileDigest{Algorithm: "sha256", Value: "utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY"}, Size: "8449"},
{Path: "Pygments-2.6.1.dist-info/RECORD"}, {Path: "Pygments-2.6.1.dist-info/RECORD"},
{Path: "pygments/__pycache__/__init__.cpython-38.pyc"}, {Path: "pygments/__pycache__/__init__.cpython-38.pyc"},
{Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"}, {Path: "pygments/util.py", Digest: &pkg.PythonFileDigest{"sha256", "586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA"}, Size: "10778"},

View File

@ -0,0 +1,28 @@
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,53 @@
Metadata-Version: 2.1
Name: Pygments
Version: 2.6.1
Summary: Pygments is a syntax highlighting package written in Python.
Home-page: https://pygments.org/
Author: Georg Brandl
Author-email: georg@python.org
LicenseFile: LICENSE.txt
Keywords: syntax highlighting
Platform: any
Classifier: License :: OSI Approved :: BSD License
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: System Administrators
Classifier: Development Status :: 6 - Mature
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Operating System :: OS Independent
Classifier: Topic :: Text Processing :: Filters
Classifier: Topic :: Utilities
Requires-Python: >=3.5
Description-Content-Type: text/markdown
Requires-Dist: soupsieve (>1.2)
Provides-Extra: html5lib
Requires-Dist: html5lib ; extra == 'html5lib'
Provides-Extra: lxml
Requires-Dist: lxml ; extra == 'lxml'
Pygments
~~~~~~~~
Pygments is a syntax highlighting package written in Python.
It is a generic syntax highlighter suitable for use in code hosting, forums,
wikis or other applications that need to prettify source code. Highlights
are:
* a wide range of over 500 languages and other text formats is supported
* special attention is paid to details, increasing quality by a fair amount
* support for new languages and formats are added easily
* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences
* it is usable as a command-line tool and as a library
:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.
:license: BSD, see LICENSE for details.

View File

@ -0,0 +1,7 @@
../../../bin/pygmentize,sha256=dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8,220
with-license-file-declared.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449
with-license-file-declared.dist-info/LICENSE.txt,sha256=utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY,8449
with-license-file-declared.dist-info/RECORD,,
pygments/__pycache__/__init__.cpython-38.pyc,,
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778
pygments/x_util.py,sha256=qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI=,10778

View File

@ -0,0 +1,28 @@
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -0,0 +1,52 @@
Metadata-Version: 2.1
Name: Pygments
Version: 2.6.1
Summary: Pygments is a syntax highlighting package written in Python.
Home-page: https://pygments.org/
Author: Georg Brandl
Author-email: georg@python.org
Keywords: syntax highlighting
Platform: any
Classifier: License :: OSI Approved :: BSD License
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: End Users/Desktop
Classifier: Intended Audience :: System Administrators
Classifier: Development Status :: 6 - Mature
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: 3.6
Classifier: Programming Language :: Python :: 3.7
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Operating System :: OS Independent
Classifier: Topic :: Text Processing :: Filters
Classifier: Topic :: Utilities
Requires-Python: >=3.5
Description-Content-Type: text/markdown
Requires-Dist: soupsieve (>1.2)
Provides-Extra: html5lib
Requires-Dist: html5lib ; extra == 'html5lib'
Provides-Extra: lxml
Requires-Dist: lxml ; extra == 'lxml'
Pygments
~~~~~~~~
Pygments is a syntax highlighting package written in Python.
It is a generic syntax highlighter suitable for use in code hosting, forums,
wikis or other applications that need to prettify source code. Highlights
are:
* a wide range of over 500 languages and other text formats is supported
* special attention is paid to details, increasing quality by a fair amount
* support for new languages and formats are added easily
* a number of output formats, presently HTML, LaTeX, RTF, SVG, all image formats that PIL supports and ANSI sequences
* it is usable as a command-line tool and as a library
:copyright: Copyright 2006-2019 by the Pygments team, see AUTHORS.
:license: BSD, see LICENSE for details.

View File

@ -0,0 +1,7 @@
../../../bin/pygmentize,sha256=dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8,220
without-license-file-declared.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449
without-license-file-declared.dist-info/LICENSE.txt,sha256=utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY,8449
without-license-file-declared.dist-info/RECORD,,
pygments/__pycache__/__init__.cpython-38.pyc,,
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778
pygments/x_util.py,sha256=qpzzsOW31KT955agi-7NS--90I0iNiJCyLJQnRCHgKI=,10778

View File

@ -0,0 +1,28 @@
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,5 +1,6 @@
../../../bin/pygmentize,sha256=dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8,220 ../../../bin/pygmentize,sha256=dDhv_U2jiCpmFQwIRHpFRLAHUO4R1jIJPEvT_QYTFp8,220
Pygments-2.6.1.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449 Pygments-2.6.1.dist-info/AUTHORS,sha256=PVpa2_Oku6BGuiUvutvuPnWGpzxqFy2I8-NIrqCvqUY,8449
Pygments-2.6.1.dist-info/LICENSE.txt,sha256=utiUvpzxqFPVpvuPnWG2_Oku6BGuay2I8-NIrqCvqUY,8449
Pygments-2.6.1.dist-info/RECORD,, Pygments-2.6.1.dist-info/RECORD,,
pygments/__pycache__/__init__.cpython-38.pyc,, pygments/__pycache__/__init__.cpython-38.pyc,,
pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778 pygments/util.py,sha256=586xXHiJGGZxqk5PMBu3vBhE68DLuAe5MBARWrSPGxA,10778

View File

@ -0,0 +1 @@
{"url": "https://github.com/python-test/test.git", "vcs_info": {"commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "vcs": "git"}}

View File

@ -0,0 +1,2 @@
pygments
something_else

View File

@ -0,0 +1 @@
{"url": "https://github.com/python-test/test.git", "vcs_info": {"commit_id": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "vcs": "git"}}

View File

@ -0,0 +1,2 @@
pygments
something_else