77# See https://aboutcode.org for more information about nexB OSS projects.
88#
99
10- from functools import partial
1110import io
1211import json
13- import logging
14- import sys
15-
16- import attr
12+ from functools import partial
1713
18- from commoncode import filetype
19- from commoncode import fileutils
2014from packagedcode import models
2115from packagedcode .utils import combine_expressions
2216
23-
2417"""
2518Parse PHP composer package manifests, see https://getcomposer.org/ and
2619https://packagist.org/
2922similar.
3023"""
3124
32- TRACE = False
33-
34-
35- def logger_debug (* args ):
36- pass
37-
3825
39- logger = logging .getLogger (__name__ )
40-
41- if TRACE :
42- logging .basicConfig (stream = sys .stdout )
43- logger .setLevel (logging .DEBUG )
44-
45- def logger_debug (* args ):
46- return logger .debug (' ' .join (isinstance (a , str ) and a or repr (a ) for a in args ))
47-
48-
49- @attr .s ()
50- class PhpComposerPackageData (models .PackageData ):
51-
52- mimetypes = ('application/json' ,)
53-
54- default_type = 'composer'
55- default_primary_language = 'PHP'
56- default_web_baseurl = 'https://packagist.org'
57- default_download_baseurl = None
58- default_api_baseurl = 'https://packagist.org/p'
26+ class BasePhpComposerHandler (models .DatafileHandler ):
5927
6028 @classmethod
61- def get_package_root (cls , manifest_resource , codebase ):
62- return manifest_resource .parent (codebase )
63-
64- def repository_homepage_url (self , baseurl = default_web_baseurl ):
65- if self .namespace :
66- return '{}/packages/{}/{}' .format (baseurl , self .namespace , self .name )
29+ def assemble (cls , package_data , resource , codebase ):
30+ datafile_name_patterns = (
31+ 'composer.json' ,
32+ 'composer.lock' ,
33+ )
34+
35+ if resource .has_parent ():
36+ dir_resource = resource .parent (codebase )
6737 else :
68- return '{}/packages/{}' . format ( baseurl , self . name )
38+ dir_resource = resource
6939
70- def api_data_url (self , baseurl = default_api_baseurl ):
71- if self .namespace :
72- return '{}/packages/{}/{}.json' .format (baseurl , self .namespace , self .name )
73- else :
74- return '{}/packages/{}.json' .format (baseurl , self .name )
40+ yield from cls .assemble_from_many_datafiles (
41+ datafile_name_patterns = datafile_name_patterns ,
42+ directory = dir_resource ,
43+ codebase = codebase ,
44+ )
45+
46+ @classmethod
47+ def assign_package_to_resources (cls , package , resource , codebase ):
48+ return super ().assign_package_to_parent_tree (package , resource , codebase )
7549
76- def compute_normalized_license (self ):
50+ @classmethod
51+ def compute_normalized_license (cls , package ):
7752 """
7853 Per https://getcomposer.org/doc/04-schema.md#license this is an expression
7954 """
80- return compute_normalized_license (self .declared_license )
81-
55+ return compute_normalized_license (package .declared_license )
8256
83- @attr .s ()
84- class ComposerJson (PhpComposerPackageData , models .PackageDataFile ):
8557
86- file_patterns = (
87- 'composer.json' ,
88- )
89- extensions = ('.json' ,)
90-
91- @classmethod
92- def is_package_data_file (cls , location ):
93- """
94- Return True if the file at ``location`` is likely a manifest of this type.
95- """
96- return filetype .is_file (location ) and fileutils .file_name (location ).lower () == 'composer.json'
58+ class PhpComposerJsonHandler (BasePhpComposerHandler ):
59+ datasource_id = 'php_composer_json'
60+ path_patterns = ('*composer.json' ,)
61+ default_package_type = 'composer'
62+ default_primary_language = 'PHP'
63+ description = 'PHP composer manifest'
64+ documentation_url = 'https://getcomposer.org/doc/04-schema.md'
9765
9866 @classmethod
99- def recognize (cls , location ):
67+ def parse (cls , location ):
10068 """
101- Yield one or more Package manifest objects given a file ``location`` pointing to a
102- package archive, manifest or similar.
69+ Yield one or more Package manifest objects given a file ``location``
70+ pointing to a package archive, manifest or similar.
10371
104- Note that this is NOT exactly the packagist .json format (all are closely related of
105- course but have important (even if minor) differences.
72+ Note that this is NOT exactly the packagist.json format (all are closely
73+ related of course but have important (even if minor) differences.
10674 """
10775 with io .open (location , encoding = 'utf-8' ) as loc :
108- package_data = json .load (loc )
76+ package_json = json .load (loc )
10977
110- yield build_package_data (cls , package_data )
78+ yield build_package_data (package_json )
11179
11280
113- def build_package_data (cls , package_data ):
114-
115- # A composer.json without name and description is not a usable PHP
116- # composer package. Name and description fields are required but
117- # only for published packages:
118- # https://getcomposer.org/doc/04-schema.md#name
119- # We want to catch both published and non-published packages here.
120- # Therefore, we use "private-package-without-a-name" as a package name if
121- # there is no name.
81+ def get_repository_homepage_url (namespace , name ):
82+ if namespace and name :
83+ return f'https://packagist.org/packages/{ namespace } /{ name } '
84+ elif name :
85+ return f'https://packagist.org/packages/{ name } '
86+
87+
88+ def get_api_data_url (namespace , name ):
89+ if namespace and name :
90+ return f'https://packagist.org/p/packages/{ namespace } /{ name } .json'
91+ elif name :
92+ return f'https://packagist.org/p/packages/{ name } .json'
93+
94+
95+ def build_package_data (package_data ):
96+
97+ # Note: A composer.json without name and description is not a usable PHP
98+ # composer package. Name and description fields are required but only for
99+ # published packages: https://getcomposer.org/doc/04-schema.md#name We want
100+ # to catch both published and non-published packages here. Therefore, we use
101+ # None as a package name if there is no name.
122102
123103 ns_name = package_data .get ('name' )
124104 is_private = False
125105 if not ns_name :
126106 ns = None
127- name = 'private-package-without-a-name'
107+ name = None
128108 is_private = True
129109 else :
130110 ns , _ , name = ns_name .rpartition ('/' )
131111
132- package = cls (
112+ package = models .PackageData (
113+ datasource_id = PhpComposerJsonHandler .datasource_id ,
114+ type = PhpComposerJsonHandler .default_package_type ,
133115 namespace = ns ,
134116 name = name ,
117+ repository_homepage_url = get_repository_homepage_url (ns , name ),
118+ api_data_url = get_api_data_url (ns , name ),
119+ primary_language = PhpComposerJsonHandler .default_primary_language ,
135120 )
136121
137122 # mapping of top level composer.json items to the Package object field name
@@ -167,50 +152,41 @@ def build_package_data(cls, package_data):
167152 ]
168153
169154 for source , func in field_mappers :
170- logger .debug ('parse: %(source)r, %(func)r' % locals ())
171155 value = package_data .get (source )
172156 if value :
173157 if isinstance (value , str ):
174158 value = value .strip ()
175159 if value :
176160 func (value , package )
161+
177162 # Parse vendor from name value
178163 vendor_mapper (package )
179- return package
180164
181- @ attr . s ()
182- class ComposerLock ( PhpComposerPackageData , models .PackageDataFile ):
165+ if not package . license_expression and package . declared_license :
166+ package . license_expression = models .compute_normalized_license ( package . declared_license )
183167
184- file_patterns = (
185- 'composer.lock' ,
186- )
187- extensions = ('.lock' ,)
168+ return package
188169
189- @classmethod
190- def is_package_data_file (cls , location ):
191- """
192- Return True if the file at ``location`` is likely a manifest of this type.
193- """
194- return filetype .is_file (location ) and fileutils .file_name (location ).lower () == 'composer.lock'
195170
196- @classmethod
197- def recognize (cls , location ):
198- """
199- Yield one or more Package manifest objects given a file ``location`` pointing to a
200- package archive, manifest or similar.
171+ class PhpComposerLockHandler (BasePhpComposerHandler ):
172+ datasource_id = 'php_composer_lock'
173+ path_patterns = ('*composer.lock' ,)
174+ default_package_type = 'composer'
175+ default_primary_language = 'PHP'
176+ description = 'PHP composer lockfile'
177+ documentation_url = 'https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control'
201178
202- Note that this is NOT exactly the packagist .json format (all are closely related of
203- course but have important (even if minor) differences.
204- """
179+ @classmethod
180+ def parse (cls , location ):
205181 with io .open (location , encoding = 'utf-8' ) as loc :
206182 package_data = json .load (loc )
207-
183+
208184 packages = [
209- build_package_data (cls , p )
185+ build_package_data (p )
210186 for p in package_data .get ('packages' , [])
211187 ]
212188 packages_dev = [
213- build_package_data (cls , p )
189+ build_package_data (p )
214190 for p in package_data .get ('packages-dev' , [])
215191 ]
216192
@@ -223,27 +199,17 @@ def recognize(cls, location):
223199 for p in packages_dev
224200 ]
225201
226- yield cls (dependencies = required_deps + required_dev_deps )
202+ yield models .PackageData (
203+ datasource_id = cls .datasource_id ,
204+ type = cls .default_package_type ,
205+ primary_language = cls .default_primary_language ,
206+ dependencies = required_deps + required_dev_deps
207+ )
227208
228209 for package in packages + packages_dev :
229210 yield package
230211
231212
232- @attr .s ()
233- class PhpPackage (PhpComposerPackageData , models .Package ):
234- """
235- A PHP Package that is created out of one/multiple python package
236- manifests.
237- """
238-
239- @property
240- def manifests (self ):
241- return [
242- ComposerJson ,
243- ComposerLock
244- ]
245-
246-
247213def compute_normalized_license (declared_license ):
248214 """
249215 Return a normalized license expression string detected from a list of
@@ -429,4 +395,3 @@ def build_dep_package(package, scope, is_runtime, is_optional):
429395 is_optional = is_optional ,
430396 is_resolved = True ,
431397 )
432-
0 commit comments