|
7 | 7 | # See https://aboutcode.org for more information about nexB OSS projects. |
8 | 8 | # |
9 | 9 |
|
| 10 | +import fnmatch |
10 | 11 | import logging |
| 12 | +import os |
11 | 13 | import sys |
12 | 14 |
|
13 | 15 | import attr |
|
23 | 25 | from commoncode.datautils import String |
24 | 26 | from commoncode.datautils import TriBoolean |
25 | 27 |
|
| 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 | + |
26 | 34 | """ |
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. |
29 | 38 |
|
30 | 39 | A package has a somewhat fuzzy definition and is code that can be consumed and |
31 | 40 | provisioned by a package manager or can be installed. |
|
44 | 53 | There are collectively named "manifests" in ScanCode. |
45 | 54 |
|
46 | 55 | 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. |
50 | 60 |
|
51 | 61 | 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 |
53 | 82 | """ |
54 | 83 |
|
55 | | -TRACE = False |
| 84 | +SCANCODE_DEBUG_PACKAGE_API = os.environ.get('SCANCODE_DEBUG_PACKAGE_API', False) |
56 | 85 |
|
| 86 | +TRACE = False or SCANCODE_DEBUG_PACKAGE_API |
57 | 87 |
|
58 | 88 | def logger_debug(*args): |
59 | 89 | pass |
60 | 90 |
|
61 | | - |
62 | 91 | logger = logging.getLogger(__name__) |
63 | 92 |
|
64 | 93 | if TRACE: |
| 94 | + import logging |
| 95 | + |
65 | 96 | logging.basicConfig(stream=sys.stdout) |
66 | 97 | logger.setLevel(logging.DEBUG) |
67 | 98 |
|
68 | 99 | def logger_debug(*args): |
69 | 100 | return logger.debug(' '.join(isinstance(a, str) and a or repr(a) for a in args)) |
70 | 101 |
|
| 102 | + logger_debug = print |
| 103 | + |
71 | 104 |
|
72 | 105 | class BaseModel(object): |
73 | 106 | """ |
@@ -386,7 +419,9 @@ class PackageFile(BaseModel): |
386 | 419 | @attr.s() |
387 | 420 | class Package(BasePackage): |
388 | 421 | """ |
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. |
390 | 425 | """ |
391 | 426 |
|
392 | 427 | # Optional. Public default type for a package class. |
@@ -564,8 +599,8 @@ def ignore_resource(cls, resource, codebase): |
564 | 599 |
|
565 | 600 | @staticmethod |
566 | 601 | 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) |
569 | 604 |
|
570 | 605 | def compute_normalized_license(self): |
571 | 606 | """ |
@@ -645,6 +680,97 @@ def compute_normalized_license(declared_license, expression_symbols=None): |
645 | 680 | # we never fail just for this |
646 | 681 | return 'unknown' |
647 | 682 |
|
| 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 | + |
648 | 774 | # Package types |
649 | 775 | # NOTE: this is somewhat redundant with extractcode archive handlers |
650 | 776 | # yet the purpose and semantics are rather different here |
|
0 commit comments