Skip to content

Commit 8d67fd8

Browse files
committed
Adopt new handler design for npm packages
We now have these data file handlers: * PhpComposerJsonHandler * PhpComposerLockHandler And we assemble these in Packages correctly Signed-off-by: Philippe Ombredanne <[email protected]>
1 parent 5a0650c commit 8d67fd8

File tree

9 files changed

+253
-251
lines changed

9 files changed

+253
-251
lines changed

src/packagedcode/phpcomposer.py

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

10-
from functools import partial
1110
import io
1211
import 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
2014
from packagedcode import models
2115
from packagedcode.utils import combine_expressions
2216

23-
2417
"""
2518
Parse PHP composer package manifests, see https://getcomposer.org/ and
2619
https://packagist.org/
@@ -29,109 +22,101 @@
2922
similar.
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-
247213
def 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-

tests/packagedcode/data/phpcomposer/a-timer/composer.json.expected

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@
4040
"license_expression": "mit",
4141
"declared_license": "MIT",
4242
"notice_text": null,
43-
"contains_source_code": null,
4443
"source_packages": [],
44+
"file_references": [],
4545
"extra_data": {},
4646
"dependencies": [
4747
{
@@ -72,9 +72,10 @@
7272
"resolved_package": {}
7373
}
7474
],
75-
"purl": "pkg:composer/jandreasn/a-timer",
7675
"repository_homepage_url": "https://packagist.org/packages/jandreasn/a-timer",
7776
"repository_download_url": null,
78-
"api_data_url": "https://packagist.org/p/packages/jandreasn/a-timer.json"
77+
"api_data_url": "https://packagist.org/p/packages/jandreasn/a-timer.json",
78+
"datasource_id": "php_composer_json",
79+
"purl": "pkg:composer/jandreasn/a-timer"
7980
}
8081
]

0 commit comments

Comments
 (0)