Skip to content

Commit a0d0010

Browse files
Create PackageManifest classes for phpcomposer
Creates PackageManifest classes for all phpcomposer manifests and overrides the methods for detection and PackageManifest creation. Signed-off-by: Ayan Sinha Mahapatra <ayansmahapatra@gmail.com>
1 parent ab20f29 commit a0d0010

4 files changed

Lines changed: 180 additions & 132 deletions

File tree

src/packagedcode/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@
5454
npm.NPMPackageJSON,
5555
npm.NPMPackageLockJSON,
5656
npm.NPMYarnLockJSON,
57-
phpcomposer.PHPComposerPackage,
57+
phpcomposer.PHPComposerJSON,
58+
phpcomposer.PHPComposerLock,
5859
haxe.HaxePackage,
5960
cargo.RustCargoCrate,
6061
cocoapods.CocoapodsPodspec,

src/packagedcode/phpcomposer.py

Lines changed: 153 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,8 @@ def logger_debug(*args):
4747

4848

4949
@attr.s()
50-
class PHPComposerPackage(models.Package, models.PackageManifest):
51-
file_patterns = (
52-
'composer.json',
53-
'composer.lock',
54-
)
55-
extensions = ('.json', '.lock',)
50+
class PHPComposerPackage(models.Package):
51+
5652
mimetypes = ('application/json',)
5753

5854
default_type = 'composer'
@@ -61,11 +57,6 @@ class PHPComposerPackage(models.Package, models.PackageManifest):
6157
default_download_baseurl = None
6258
default_api_baseurl = 'https://packagist.org/p'
6359

64-
@classmethod
65-
def recognize(cls, location):
66-
for package in parse(location):
67-
yield package
68-
6960
@classmethod
7061
def get_package_root(cls, manifest_resource, codebase):
7162
return manifest_resource.parent(codebase)
@@ -89,6 +80,157 @@ def compute_normalized_license(self):
8980
return compute_normalized_license(self.declared_license)
9081

9182

83+
@attr.s()
84+
class PHPComposerJSON(PHPComposerPackage, models.PackageManifest):
85+
86+
file_patterns = (
87+
'composer.json',
88+
)
89+
extensions = ('.json',)
90+
manifest_type = 'composerjson'
91+
92+
@classmethod
93+
def is_manifest(cls, location):
94+
"""
95+
Return True if the file at ``location`` is likely a manifest of this type.
96+
"""
97+
return filetype.is_file(location) and fileutils.file_name(location).lower() == 'composer.json'
98+
99+
@classmethod
100+
def recognize(cls, location):
101+
"""
102+
Yield one or more Package manifest objects given a file ``location`` pointing to a
103+
package archive, manifest or similar.
104+
105+
Note that this is NOT exactly the packagist .json format (all are closely related of
106+
course but have important (even if minor) differences.
107+
"""
108+
with io.open(location, encoding='utf-8') as loc:
109+
package_data = json.load(loc)
110+
111+
yield cls.build_package_manifest(package_data)
112+
113+
@classmethod
114+
def build_package_manifest(cls, package_data):
115+
116+
# A composer.json without name and description is not a usable PHP
117+
# composer package. Name and description fields are required but
118+
# only for published packages:
119+
# https://getcomposer.org/doc/04-schema.md#name
120+
# We want to catch both published and non-published packages here.
121+
# Therefore, we use "private-package-without-a-name" as a package name if
122+
# there is no name.
123+
124+
ns_name = package_data.get('name')
125+
is_private = False
126+
if not ns_name:
127+
ns = None
128+
name = 'private-package-without-a-name'
129+
is_private = True
130+
else:
131+
ns, _, name = ns_name.rpartition('/')
132+
133+
package = PHPComposerPackage(
134+
namespace=ns,
135+
name=name,
136+
)
137+
138+
# mapping of top level composer.json items to the Package object field name
139+
plain_fields = [
140+
('version', 'version'),
141+
('description', 'summary'),
142+
('keywords', 'keywords'),
143+
('homepage', 'homepage_url'),
144+
]
145+
146+
for source, target in plain_fields:
147+
value = package_data.get(source)
148+
if isinstance(value, str):
149+
value = value.strip()
150+
if value:
151+
setattr(package, target, value)
152+
153+
# mapping of top level composer.json items to a function accepting as
154+
# arguments the composer.json element value and returning an iterable of
155+
# key, values Package Object to update
156+
field_mappers = [
157+
('authors', author_mapper),
158+
('license', partial(licensing_mapper, is_private=is_private)),
159+
('support', support_mapper),
160+
('require', partial(_deps_mapper, scope='require', is_runtime=True)),
161+
('require-dev', partial(_deps_mapper, scope='require-dev', is_optional=True)),
162+
('provide', partial(_deps_mapper, scope='provide', is_runtime=True)),
163+
('conflict', partial(_deps_mapper, scope='conflict', is_runtime=True, is_optional=True)),
164+
('replace', partial(_deps_mapper, scope='replace', is_runtime=True, is_optional=True)),
165+
('suggest', partial(_deps_mapper, scope='suggest', is_runtime=True, is_optional=True)),
166+
('source', source_mapper),
167+
('dist', dist_mapper)
168+
]
169+
170+
for source, func in field_mappers:
171+
logger.debug('parse: %(source)r, %(func)r' % locals())
172+
value = package_data.get(source)
173+
if value:
174+
if isinstance(value, str):
175+
value = value.strip()
176+
if value:
177+
func(value, package)
178+
# Parse vendor from name value
179+
vendor_mapper(package)
180+
return package
181+
182+
@attr.s()
183+
class PHPComposerLock(PHPComposerPackage, models.PackageManifest):
184+
185+
file_patterns = (
186+
'composer.lock',
187+
)
188+
extensions = ('.lock',)
189+
manifest_type = 'composerlock'
190+
191+
@classmethod
192+
def is_manifest(cls, location):
193+
"""
194+
Return True if the file at ``location`` is likely a manifest of this type.
195+
"""
196+
return filetype.is_file(location) and fileutils.file_name(location).lower() == 'composer.lock'
197+
198+
@classmethod
199+
def recognize(cls, location):
200+
"""
201+
Yield one or more Package manifest objects given a file ``location`` pointing to a
202+
package archive, manifest or similar.
203+
204+
Note that this is NOT exactly the packagist .json format (all are closely related of
205+
course but have important (even if minor) differences.
206+
"""
207+
with io.open(location, encoding='utf-8') as loc:
208+
package_data = json.load(loc)
209+
210+
packages = [
211+
PHPComposerJSON.build_package_manifest(p)
212+
for p in package_data.get('packages', [])
213+
]
214+
packages_dev = [
215+
PHPComposerJSON.build_package_manifest(p)
216+
for p in package_data.get('packages-dev', [])
217+
]
218+
219+
required_deps = [
220+
build_dep_package(p, scope='require', is_runtime=True, is_optional=False)
221+
for p in packages
222+
]
223+
required_dev_deps = [
224+
build_dep_package(p, scope='require-dev', is_runtime=False, is_optional=True)
225+
for p in packages_dev
226+
]
227+
228+
yield PHPComposerPackage(dependencies=required_deps + required_dev_deps)
229+
230+
for package in packages + packages_dev:
231+
yield package
232+
233+
92234
def compute_normalized_license(declared_license):
93235
"""
94236
Return a normalized license expression string detected from a list of
@@ -121,103 +263,6 @@ def compute_normalized_license(declared_license):
121263
return combine_expressions(detected_licenses, 'OR')
122264

123265

124-
def is_phpcomposer_json(location):
125-
return filetype.is_file(location) and fileutils.file_name(location).lower() == 'composer.json'
126-
127-
128-
def is_phpcomposer_lock(location):
129-
return filetype.is_file(location) and fileutils.file_name(location).lower() == 'composer.lock'
130-
131-
132-
def parse(location):
133-
"""
134-
Yield Package objects from a composer.json or composer.lock file. Note that
135-
this is NOT exactly the packagist .json format (all are closely related of
136-
course but have important (even if minor) differences.
137-
"""
138-
if is_phpcomposer_json(location):
139-
with io.open(location, encoding='utf-8') as loc:
140-
package_data = json.load(loc)
141-
yield build_package_from_json(package_data)
142-
143-
elif is_phpcomposer_lock(location):
144-
with io.open(location, encoding='utf-8') as loc:
145-
package_data = json.load(loc)
146-
for package in build_packages_from_lock(package_data):
147-
yield package
148-
149-
150-
def build_package_from_json(package_data):
151-
"""
152-
Return a composer Package object from a package data mapping or None.
153-
"""
154-
# A composer.json without name and description is not a usable PHP
155-
# composer package. Name and description fields are required but
156-
# only for published packages:
157-
# https://getcomposer.org/doc/04-schema.md#name
158-
# We want to catch both published and non-published packages here.
159-
# Therefore, we use "private-package-without-a-name" as a package name if
160-
# there is no name.
161-
162-
ns_name = package_data.get('name')
163-
is_private = False
164-
if not ns_name:
165-
ns = None
166-
name = 'private-package-without-a-name'
167-
is_private = True
168-
else:
169-
ns, _, name = ns_name.rpartition('/')
170-
171-
package = PHPComposerPackage(
172-
namespace=ns,
173-
name=name,
174-
)
175-
176-
# mapping of top level composer.json items to the Package object field name
177-
plain_fields = [
178-
('version', 'version'),
179-
('description', 'summary'),
180-
('keywords', 'keywords'),
181-
('homepage', 'homepage_url'),
182-
]
183-
184-
for source, target in plain_fields:
185-
value = package_data.get(source)
186-
if isinstance(value, str):
187-
value = value.strip()
188-
if value:
189-
setattr(package, target, value)
190-
191-
# mapping of top level composer.json items to a function accepting as
192-
# arguments the composer.json element value and returning an iterable of
193-
# key, values Package Object to update
194-
field_mappers = [
195-
('authors', author_mapper),
196-
('license', partial(licensing_mapper, is_private=is_private)),
197-
('support', support_mapper),
198-
('require', partial(_deps_mapper, scope='require', is_runtime=True)),
199-
('require-dev', partial(_deps_mapper, scope='require-dev', is_optional=True)),
200-
('provide', partial(_deps_mapper, scope='provide', is_runtime=True)),
201-
('conflict', partial(_deps_mapper, scope='conflict', is_runtime=True, is_optional=True)),
202-
('replace', partial(_deps_mapper, scope='replace', is_runtime=True, is_optional=True)),
203-
('suggest', partial(_deps_mapper, scope='suggest', is_runtime=True, is_optional=True)),
204-
('source', source_mapper),
205-
('dist', dist_mapper)
206-
]
207-
208-
for source, func in field_mappers:
209-
logger.debug('parse: %(source)r, %(func)r' % locals())
210-
value = package_data.get(source)
211-
if value:
212-
if isinstance(value, str):
213-
value = value.strip()
214-
if value:
215-
func(value, package)
216-
# Parse vendor from name value
217-
vendor_mapper(package)
218-
return package
219-
220-
221266
def licensing_mapper(licenses, package, is_private=False):
222267
"""
223268
Update package licensing and return package.
@@ -372,16 +417,3 @@ def build_dep_package(package, scope, is_runtime, is_optional):
372417
is_resolved=True,
373418
)
374419

375-
376-
def build_packages_from_lock(package_data):
377-
"""
378-
Yield composer Package objects from a package data mapping that originated
379-
from a composer.lock file
380-
"""
381-
packages = [build_package_from_json(p) for p in package_data.get('packages', [])]
382-
packages_dev = [build_package_from_json(p) for p in package_data.get('packages-dev', [])]
383-
required_deps = [build_dep_package(p, scope='require', is_runtime=True, is_optional=False) for p in packages]
384-
required_dev_deps = [build_dep_package(p, scope='require-dev', is_runtime=False, is_optional=True) for p in packages_dev]
385-
yield PHPComposerPackage(dependencies=required_deps + required_dev_deps)
386-
for package in packages + packages_dev:
387-
yield package

tests/packagedcode/data/plugin/help.txt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,9 +73,15 @@ Package: chrome
7373

7474
--------------------------------------------
7575
Package: composer
76-
class: packagedcode.phpcomposer:PHPComposerPackage
77-
file_patterns: composer.json, composer.lock
78-
extensions: .json, .lock
76+
class: packagedcode.phpcomposer:PHPComposerJSON
77+
file_patterns: composer.json
78+
extensions: .json
79+
80+
--------------------------------------------
81+
Package: composer
82+
class: packagedcode.phpcomposer:PHPComposerLock
83+
file_patterns: composer.lock
84+
extensions: .lock
7985

8086
--------------------------------------------
8187
Package: conda

tests/packagedcode/test_phpcomposer.py

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,44 +35,53 @@ def test_parse_person(self):
3535
('Jordi Boggiano', 'Developer', 'j.boggiano@seld.be', 'http://seld.be')
3636
]
3737
assert list(phpcomposer.parse_person(test)) == expected
38+
39+
def test_is_manifest_php_composer_json(self):
40+
test_file = self.get_test_loc('phpcomposer/a-timer/composer.json')
41+
assert phpcomposer.PHPComposerJSON.is_manifest(test_file)
42+
3843
def test_parse_atimer(self):
3944
test_file = self.get_test_loc('phpcomposer/a-timer/composer.json')
4045
expected_loc = self.get_test_loc('phpcomposer/a-timer/composer.json.expected')
41-
packages = phpcomposer.parse(test_file)
46+
packages = phpcomposer.PHPComposerJSON.recognize(test_file)
4247
self.check_packages(packages, expected_loc, regen=False)
4348

4449
def test_parse_framework(self):
4550
test_file = self.get_test_loc('phpcomposer/framework/composer.json')
4651
expected_loc = self.get_test_loc('phpcomposer/framework/composer.json.expected')
47-
packages = phpcomposer.parse(test_file)
52+
packages = phpcomposer.PHPComposerJSON.recognize(test_file)
4853
self.check_packages(packages, expected_loc, regen=False)
4954

5055
def test_parse_slim(self):
5156
test_file = self.get_test_loc('phpcomposer/slim/composer.json')
5257
expected_loc = self.get_test_loc('phpcomposer/slim/composer.json.expected')
53-
packages = phpcomposer.parse(test_file)
58+
packages = phpcomposer.PHPComposerJSON.recognize(test_file)
5459
self.check_packages(packages, expected_loc, regen=False)
5560

5661
def test_parse_modern(self):
5762
test_file = self.get_test_loc('phpcomposer/modern/composer.json')
5863
expected_loc = self.get_test_loc('phpcomposer/modern/composer.json.expected')
59-
packages = phpcomposer.parse(test_file)
64+
packages = phpcomposer.PHPComposerJSON.recognize(test_file)
6065
self.check_packages(packages, expected_loc, regen=False)
6166

6267
def test_parse_fake_license1(self):
6368
test_file = self.get_test_loc('phpcomposer/fake/composer.json')
6469
expected_loc = self.get_test_loc('phpcomposer/fake/composer.json.expected')
65-
packages = phpcomposer.parse(test_file)
70+
packages = phpcomposer.PHPComposerJSON.recognize(test_file)
6671
self.check_packages(packages, expected_loc, regen=False)
6772

6873
def test_parse_fake_license2(self):
6974
test_file = self.get_test_loc('phpcomposer/fake2/composer.json')
7075
expected_loc = self.get_test_loc('phpcomposer/fake2/composer.json.expected')
71-
packages = phpcomposer.parse(test_file)
76+
packages = phpcomposer.PHPComposerJSON.recognize(test_file)
7277
self.check_packages(packages, expected_loc, regen=False)
7378

79+
def test_is_manifest_php_composer_lock(self):
80+
test_file = self.get_test_loc('phpcomposer/composer.lock')
81+
assert phpcomposer.PHPComposerLock.is_manifest(test_file)
82+
7483
def test_parse_composer_lock(self):
7584
test_file = self.get_test_loc('phpcomposer/composer.lock')
7685
expected_loc = self.get_test_loc('phpcomposer/composer.lock-expected.json')
77-
packages = phpcomposer.parse(test_file)
86+
packages = phpcomposer.PHPComposerLock.recognize(test_file)
7887
self.check_packages(packages, expected_loc, regen=False)

0 commit comments

Comments
 (0)