Skip to content

Commit 1a4bd31

Browse files
Add PackageManifest class to models
Adds a mixin class PackageManifest to models which will have the base implementations of Package Manifest specific methods for recognizing and creating ecosystem specific package manifest objects, which will inherit from this class and the ecosystem Package class. Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent d5b09c3 commit 1a4bd31

4 files changed

Lines changed: 171 additions & 86 deletions

File tree

src/packagedcode/__init__.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838

3939
# Note: the order matters: from the most to the least specific
4040
# Package classes MUST be added to this list to be active
41-
PACKAGE_TYPES = [
41+
PACKAGE_MANIFEST_TYPES = [
4242
rpm.RpmPackage,
4343
debian.DebianPackage,
4444

@@ -90,21 +90,27 @@
9090
pubspec.PubspecPackage,
9191
]
9292

93-
PACKAGES_BY_TYPE = {cls.default_type: cls for cls in PACKAGE_TYPES}
94-
93+
PACKAGE_MANIFESTS_BY_TYPE = {
94+
(
95+
cls.package_manifest_type
96+
if isinstance(cls, models.PackageManifest)
97+
else cls.default_type
98+
): cls
99+
for cls in PACKAGE_MANIFEST_TYPES
100+
}
95101
# We cannot have two package classes with the same type
96-
if len(PACKAGES_BY_TYPE) != len(PACKAGE_TYPES):
102+
if len(PACKAGE_MANIFESTS_BY_TYPE) != len(PACKAGE_MANIFEST_TYPES):
97103
seen_types = {}
98-
for pt in PACKAGE_TYPES:
99-
assert pt.default_type
100-
seen = seen_types.get(pt.default_type)
104+
for pmt in PACKAGE_MANIFEST_TYPES:
105+
assert pmt.default_type
106+
seen = seen_types.get(pmt.default_type)
101107
if seen:
102108
msg = ('Invalid duplicated packagedcode.Package types: '
103109
'"{}:{}" and "{}:{}" have the same type.'
104-
.format(pt.default_type, pt.__name__, seen.default_type, seen.__name__,))
110+
.format(pmt.default_type, pmt.__name__, seen.default_type, seen.__name__,))
105111
raise Exception(msg)
106112
else:
107-
seen_types[pt.default_type] = pt
113+
seen_types[pmt.default_type] = pmt
108114

109115

110116
def get_package_class(scan_data, default=models.Package):
@@ -130,7 +136,7 @@ def get_package_class(scan_data, default=models.Package):
130136
if not ptype:
131137
# basic type for default package types
132138
return default
133-
ptype_class = PACKAGES_BY_TYPE.get(ptype)
139+
ptype_class = PACKAGE_MANIFESTS_BY_TYPE.get(ptype)
134140
return ptype_class or default
135141

136142

src/packagedcode/models.py

Lines changed: 137 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
import fnmatch
1011
import logging
12+
import os
1113
import sys
1214

1315
import attr
@@ -23,9 +25,16 @@
2325
from commoncode.datautils import String
2426
from commoncode.datautils import TriBoolean
2527

28+
from commoncode import filetype
29+
from commoncode.fileutils import file_name
30+
from commoncode.fileutils import splitext_name
31+
from typecode import contenttype
32+
33+
2634
"""
27-
Data models for package information and dependencies, abstracting the
28-
differences existing between package formats and tools.
35+
Data models for package information and dependencies, and also data models
36+
for package manifests for abstracting the differences existing between
37+
package formats and tools.
2938
3039
A package has a somewhat fuzzy definition and is code that can be consumed and
3140
provisioned by a package manager or can be installed.
@@ -44,30 +53,54 @@
4453
There are collectively named "manifests" in ScanCode.
4554
4655
We handle package information at two levels:
47-
1.- package information collected in a "manifest" at a file level
48-
2.- aggregated package information based on "manifest" at a directory or archive
49-
level (or in some rarer cases file level)
56+
57+
1. package manifest information collected in a "manifest" at a file level
58+
2. package instances at the codebase level, where a package instance contains
59+
one or more package manifests, and files for that package.
5060
5161
The second requires the first to be computed.
52-
The schema for these two is the same.
62+
63+
Class Hierarchy:
64+
65+
Base Classes: (Classes to be inherited)
66+
67+
- Package:
68+
Base Data class with package data
69+
- PackageManifest:
70+
Mixin class with manifest specific data and methods
71+
- PackageInstance:
72+
Mixin class with package instance specific data and methods
73+
74+
- Package Ecosystem: (Classes which would have object instances)
75+
76+
- EcosystemPackage(Package):
77+
A class with ecosystem specific data (and data specific methods)
78+
- EcosystemPackageManifest(EcosystemPackage, PackageManifest):
79+
A class that overides and implements package manifest methods for that ecosystem
80+
- EcosystemPackageInstance(EcosystemPackage, PackageInstance):
81+
A class that overrides and implements package instance methods for that ecosystem
5382
"""
5483

55-
TRACE = False
84+
SCANCODE_DEBUG_PACKAGE_API = os.environ.get('SCANCODE_DEBUG_PACKAGE_API', False)
5685

86+
TRACE = False or SCANCODE_DEBUG_PACKAGE_API
5787

5888
def logger_debug(*args):
5989
pass
6090

61-
6291
logger = logging.getLogger(__name__)
6392

6493
if TRACE:
94+
import logging
95+
6596
logging.basicConfig(stream=sys.stdout)
6697
logger.setLevel(logging.DEBUG)
6798

6899
def logger_debug(*args):
69100
return logger.debug(' '.join(isinstance(a, str) and a or repr(a) for a in args))
70101

102+
logger_debug = print
103+
71104

72105
class BaseModel(object):
73106
"""
@@ -386,7 +419,9 @@ class PackageFile(BaseModel):
386419
@attr.s()
387420
class Package(BasePackage):
388421
"""
389-
A package object as represented by its manifest data.
422+
A package object as represented by either data from one of its different types of
423+
package manifests or that of a package instance created from one or more of these
424+
package manifests, and files for that package.
390425
"""
391426

392427
# Optional. Public default type for a package class.
@@ -564,8 +599,8 @@ def ignore_resource(cls, resource, codebase):
564599

565600
@staticmethod
566601
def is_ignored_package_resource(resource, codebase):
567-
from packagedcode import PACKAGE_TYPES
568-
return any(pt.ignore_resource(resource, codebase) for pt in PACKAGE_TYPES)
602+
from packagedcode import PACKAGE_MANIFEST_TYPES
603+
return any(pt.ignore_resource(resource, codebase) for pt in PACKAGE_MANIFEST_TYPES)
569604

570605
def compute_normalized_license(self):
571606
"""
@@ -645,6 +680,97 @@ def compute_normalized_license(declared_license, expression_symbols=None):
645680
# we never fail just for this
646681
return 'unknown'
647682

683+
684+
class PackageManifest:
685+
"""
686+
A mixin for package manifest that can be recognized.
687+
688+
Subclasses must extend a Package subclass for a given ecosystem.
689+
"""
690+
691+
# class-level attributes used to recognize a package
692+
filetypes = tuple()
693+
mimetypes = tuple()
694+
extensions = tuple()
695+
696+
# list of known file_patterns for a package manifest type
697+
file_patterns = tuple()
698+
699+
# Package manifest type within a package ecosystem
700+
manifest_type = None
701+
702+
@property
703+
def package_manifest_type(self):
704+
"""
705+
A tuple unique across package manifests, created from the default package type
706+
and the manifest type.
707+
"""
708+
return self.default_type, self.manifest_type
709+
710+
@classmethod
711+
def is_manifest(cls, location):
712+
"""
713+
Return True if the file at ``location`` is likely a manifest of this type.
714+
715+
Sub-classes should override to implement their own manifest recognition.
716+
"""
717+
if not filetype.is_file(location):
718+
return
719+
720+
filename = file_name(location)
721+
722+
file_patterns = cls.file_patterns
723+
if any(fnmatch.fnmatchcase(filename, metaf) for metaf in file_patterns):
724+
return True
725+
726+
T = contenttype.get_type(location)
727+
ftype = T.filetype_file.lower()
728+
mtype = T.mimetype_file
729+
730+
_base_name, extension = splitext_name(location, is_file=True)
731+
extension = extension.lower()
732+
733+
if TRACE:
734+
logger_debug(
735+
'is_manifest: ftype:', ftype, 'mtype:', mtype,
736+
'pygtype:', T.filetype_pygment,
737+
'fname:', filename, 'ext:', extension,
738+
)
739+
740+
type_matched = False
741+
if cls.filetypes:
742+
type_matched = any(t in ftype for t in cls.filetypes)
743+
744+
mime_matched = False
745+
if cls.mimetypes:
746+
mime_matched = any(m in mtype for m in cls.mimetypes)
747+
748+
extension_matched = False
749+
extensions = cls.extensions
750+
if extensions:
751+
extensions = (e.lower() for e in extensions)
752+
extension_matched = any(
753+
fnmatch.fnmatchcase(extension, ext_pat)
754+
for ext_pat in extensions
755+
)
756+
757+
if type_matched and mime_matched and extension_matched:
758+
return True
759+
760+
@classmethod
761+
def recognize(cls, location):
762+
"""
763+
Yield one or more Package manifest objects given a file at `location`
764+
pointing to a package archive, manifest or similar.
765+
766+
Sub-classes should override to implement their own package recognition and creation.
767+
768+
This should be called on the file at `location` only if `is_manifest` function
769+
of the same class returns True.
770+
"""
771+
raise NotImplementedError
772+
773+
648774
# Package types
649775
# NOTE: this is somewhat redundant with extractcode archive handlers
650776
# yet the purpose and semantics are rather different here

src/packagedcode/plugin_package.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@
1818
from commoncode.cliutils import SCAN_GROUP
1919

2020
from packagedcode import get_package_instance
21-
from packagedcode import PACKAGE_TYPES
21+
from packagedcode import PACKAGE_MANIFEST_TYPES
2222

2323

2424
def print_packages(ctx, param, value):
2525
if not value or ctx.resilient_parsing:
2626
return
27-
for package_cls in sorted(PACKAGE_TYPES, key=lambda pc: (pc.default_type)):
27+
for package_cls in sorted(PACKAGE_MANIFEST_TYPES, key=lambda pc: (pc.default_type)):
2828
click.echo('--------------------------------------------')
2929
click.echo('Package: {self.default_type}'.format(self=package_cls))
3030
click.echo(

src/packagedcode/recognize.py

Lines changed: 16 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from commoncode import filetype
1515
from commoncode.fileutils import file_name
1616
from commoncode.fileutils import splitext_name
17-
from packagedcode import PACKAGE_TYPES
17+
from packagedcode import PACKAGE_MANIFEST_TYPES
1818
from typecode import contenttype
1919

2020
SCANCODE_DEBUG_PACKAGE_API = os.environ.get('SCANCODE_DEBUG_PACKAGE_API', False)
@@ -52,83 +52,36 @@ def recognize_package_manifests(location):
5252
if not filetype.is_file(location):
5353
return
5454

55-
T = contenttype.get_type(location)
56-
ftype = T.filetype_file.lower()
57-
mtype = T.mimetype_file
58-
59-
_base_name, extension = splitext_name(location, is_file=True)
60-
filename = file_name(location)
61-
extension = extension.lower()
62-
63-
if TRACE:
64-
logger_debug(
65-
'recognize_packages: ftype:', ftype, 'mtype:', mtype,
66-
'pygtype:', T.filetype_pygment,
67-
'fname:', filename, 'ext:', extension,
68-
)
69-
7055
recognized_package_manifests = []
71-
for package_type in PACKAGE_TYPES:
72-
# Note: default to True if there is nothing to match against
73-
metafiles = package_type.metafiles
74-
if any(fnmatch.fnmatchcase(filename, metaf) for metaf in metafiles):
75-
for recognized in package_type.recognize(location):
76-
if TRACE:
77-
logger_debug(
78-
'recognize_packages: metafile matching: recognized:',
79-
recognized,
80-
)
81-
if recognized and not recognized.license_expression:
82-
# compute and set a normalized license expression
83-
recognized.license_expression = recognized.compute_normalized_license()
56+
for package_manifest_type in PACKAGE_MANIFEST_TYPES:
57+
if package_manifest_type.is_manifest(location):
58+
try:
59+
for recognized in package_manifest_type.recognize(location):
8460
if TRACE:
8561
logger_debug(
86-
'recognize_packages: recognized.license_expression:',
87-
recognized.license_expression,
62+
'recognize_packages: metafile matching: recognized:',
63+
recognized,
8864
)
89-
recognized_package_manifests.append(recognized)
90-
return recognized_package_manifests
91-
92-
type_matched = False
93-
if package_type.filetypes:
94-
type_matched = any(t in ftype for t in package_type.filetypes)
95-
96-
mime_matched = False
97-
if package_type.mimetypes:
98-
mime_matched = any(m in mtype for m in package_type.mimetypes)
99-
100-
extension_matched = False
101-
extensions = package_type.extensions
102-
if extensions:
103-
extensions = (e.lower() for e in extensions)
104-
extension_matched = any(
105-
fnmatch.fnmatchcase(extension, ext_pat)
106-
for ext_pat in extensions
107-
)
108-
109-
if type_matched and mime_matched and extension_matched:
110-
if TRACE:
111-
logger_debug(f'recognize_packages: all matching for {package_type}')
112-
113-
try:
114-
for recognized in package_type.recognize(location):
115-
# compute and set a normalized license expression
11665
if recognized and not recognized.license_expression:
66+
# compute and set a normalized license expression
11767
try:
11868
recognized.license_expression = recognized.compute_normalized_license()
11969
except Exception:
12070
if SCANCODE_DEBUG_PACKAGE_API:
12171
raise
12272
recognized.license_expression = 'unknown'
12373

124-
if TRACE:
125-
logger_debug('recognize_packages: recognized', recognized)
126-
74+
if TRACE:
75+
logger_debug(
76+
'recognize_packages: recognized.license_expression:',
77+
recognized.license_expression,
78+
)
12779
recognized_package_manifests.append(recognized)
80+
return recognized_package_manifests
12881

12982
except NotImplementedError:
13083
# build a plain package if recognize is not yet implemented
131-
recognized = package_type()
84+
recognized = package_manifest_type()
13285
if TRACE:
13386
logger_debug('recognize_packages: recognized', recognized)
13487

@@ -139,4 +92,4 @@ def recognize_package_manifests(location):
13992

14093
return recognized_package_manifests
14194

142-
if TRACE: logger_debug('recognize_packages: no match for type:', package_type)
95+
if TRACE: logger_debug('recognize_packages: no match for type:', package_manifest_type)

0 commit comments

Comments
 (0)