Skip to content

Commit

Permalink
Add a PHP PECL finder
Browse files Browse the repository at this point in the history
Searches the index at pecl.php.net for the requested package.

Drive-by changes:
 - Add testing on Python 3.12
 - Ensure the local hatch env uses the -cli requirements
 - Fix the README
  • Loading branch information
juledwar committed Oct 9, 2024
1 parent cf774f7 commit 800ba93
Show file tree
Hide file tree
Showing 10 changed files with 178 additions and 11 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ['3.8', '3.9', '3.10', '3.11']
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v2
Expand Down
4 changes: 0 additions & 4 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@
"source.organizeImports": "explicit"
}
},
"black-formatter.args": [
"--line-length",
"79"
],
"python.testing.pytestEnabled": false,
"python.testing.unittestEnabled": true,
"python.testing.unittestArgs": [
Expand Down
6 changes: 4 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@ Currently supported finders are:
- Golang modules
- Java JARs
- Ruby Gems
- Rust Crates
- PHP PECL packages

If you want to download Alpine packages, you must have `git` installed.


Requirements
------------
Soufi is currently tested on Python versions 3.8 through 3.11. It is
Soufi is currently tested on Python versions 3.8 through 3.12. It is
known not to work on 3.6.


Expand Down Expand Up @@ -114,5 +116,5 @@ for details on backend configuration.
Copyright
---------

Soufi is copyright (c) 2021-2023 Cisco Systems, Inc. and its affiliates
Soufi is copyright (c) 2021-2024 Cisco Systems, Inc. and its affiliates
All rights reserved.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ features = ["test"]
# Bootstrap or upgrade an existing virtualenv:
scripts.install = [
# Install exactly what's in the locked requirements and nothing else.
"pip install -U --no-deps -r requirements/requirements-test.txt",
"pip install -U --no-deps -r requirements/requirements-cli.txt",
]
scripts.py3 = [
"export PYTHON='coverage run --source soufi --parallel-mode'; stestr run {args}",
Expand Down
15 changes: 13 additions & 2 deletions soufi/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,17 @@ def crate(cls, name, version, timeout=None):
)
return cls.find(crate_finder)

@classmethod
def phppecl(cls, name, version, timeout=None):
pecl_finder = finder.factory(
"phppecl",
name=name,
version=version,
s_type=finder.SourceType.phppecl,
timeout=timeout,
)
return cls.find(pecl_finder)


def make_archive_from_discovery_source(disc_src, fname):
try:
Expand Down Expand Up @@ -295,8 +306,8 @@ def main(
archive is used instead.
The sources currently supported are 'debian', 'ubuntu', 'rhel', 'centos',
'alpine', 'photon', 'java', 'go', 'python', and 'npm', one of which must be
specified as the DISTRO argument.
'alpine', 'photon', 'java', 'go', 'python', 'create', 'phppecl', and 'npm',
one of which must be specified as the DISTRO argument.
"""
try:
func = functools.partial(
Expand Down
1 change: 1 addition & 0 deletions soufi/finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class SourceType(enum.Enum):
go = "go"
nuget = "nuget"
crate = "crate"
phppecl = "phppecl"


@enum.unique
Expand Down
54 changes: 54 additions & 0 deletions soufi/finders/php_pecl.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright (c) 2024 Cisco Systems, Inc. and its affiliates
# All rights reserved.


import defusedxml.lxml
import requests

from soufi import exceptions, finder

DEFAULT_INDEX = "https://pecl.php.net/"


class PHPPECL(finder.SourceFinder):
"""Find PHP PECL packages.
Looks up the package in the PECL index and returns the URL for the package.
"""

distro = finder.SourceType.phppecl.value

def _find(self):
source_url = self.get_source_url()
return PHPPECLDiscoveredSource([source_url], timeout=self.timeout)

def get_source_url(self):
"""Examine the index to find the source URL for the package.
This is simply a matter of using the index's REST API to do a package
query, and returning a URL contained in the returned XML data.
"""
url = f"{DEFAULT_INDEX}rest/r/{self.name}/{self.version}.xml"
# The returned XML document contains a <g> element with the URL.
try:
with requests.get(url, stream=True, timeout=30) as r:
r.raw.decode_content = True
xml = defusedxml.lxml.parse(r.raw)
return xml.find('.//{*}g').text
except Exception:
raise exceptions.SourceNotFound


class PHPPECLDiscoveredSource(finder.DiscoveredSource):
"""A discovered Rust Crate package."""

make_archive = finder.DiscoveredSource.remote_url_is_archive
archive_extension = ".tgz"

def populate_archive(self, *args, **kwargs): # pragma: no cover
# Required by the base class but PECL archives are already tarballs so
# nothing to do.
pass

def __repr__(self):
return self.urls[0]
24 changes: 24 additions & 0 deletions soufi/testing/data/phppecl/ncurses_1.0.2.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<r xmlns="http://pear.php.net/dtd/rest.release" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="http://pear.php.net/dtd/rest.release http://pear.php.net/dtd/rest.release.xsd">
<p xlink:href="/rest/p/ncurses">ncurses</p>
<c>pecl.php.net</c>
<v>1.0.2</v>
<st>stable</st>
<l>PHP</l>
<m>felipe</m>
<s>Terminal screen handling and optimization package</s>
<d>ncurses (new curses) is a free software emulation of curses in
System V Rel 4.0 (and above). It uses terminfo format, supports
pads, colors, multiple highlights, form characters and function
key mapping. Because of the interactive nature of this library,
it will be of little use for writing Web applications, but may
be useful when writing scripts meant using PHP from the command
line.
See also http://www.gnu.org/software/ncurses/ncurses.html</d>
<da>2012-06-16 13:07:13</da>
<n>- Fixed build on PHP 5.3+
- Fixed bug #60853 (Missing NCURSES_KEY_HOME constant)</n>
<f>16226</f>
<g>https://pecl.php.net/get/ncurses-1.0.2</g>
<x xlink:href="package.1.0.2.xml"/>
</r>
78 changes: 78 additions & 0 deletions soufi/tests/finders/test_phppecl_finder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Copyright (c) 2024 Cisco Systems, Inc. and its affiliates
# All rights reserved.

from pathlib import Path
from unittest import mock

import requests

from soufi import exceptions, testing
from soufi.finder import SourceType
from soufi.finders import php_pecl
from soufi.testing import base


class TestPHPPECLFinder(base.TestCase):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.testing = Path(testing.__path__[0]) / 'data' / 'phppecl'

def make_finder(self, name=None, version=None):
if name is None:
name = self.factory.make_string("name")
if version is None:
version = self.factory.make_string("version")
kwargs = dict(name=name, version=version, s_type=SourceType.phppecl)
return php_pecl.PHPPECL(**kwargs)

def test_get_source_url(self):
# The test data uses this package at this version.
finder = self.make_finder("ncurses", "1.0.2")
get = self.patch_get_with_response(requests.codes.ok)
fp = open(self.testing / "ncurses_1.0.2.xml", "rb")
self.addCleanup(fp.close)
# Patch the get context manager to return the file stream.
get.return_value.__enter__.return_value.raw = fp

found_url = finder.get_source_url()
expected_url = (
f"{php_pecl.DEFAULT_INDEX}get/{finder.name}-{finder.version}"
)
self.assertEqual(expected_url, found_url)
call = mock.call(
f"{php_pecl.DEFAULT_INDEX}rest/r/"
f"{finder.name}/{finder.version}.xml",
stream=True,
timeout=30,
)
self.assertIn(call, get.call_args_list)

def test_get_source_url_source_not_found(self):
finder = self.make_finder()
self.patch_get_with_response(requests.codes.not_found)
self.assertRaises(exceptions.SourceNotFound, finder.get_source_url)

def test_find(self):
finder = self.make_finder()
url = self.factory.make_url()
self.patch(finder, "get_source_url").return_value = url

disc_source = finder.find()
self.assertIsInstance(disc_source, php_pecl.PHPPECLDiscoveredSource)
self.assertEqual([url], disc_source.urls)


class TestPHPPECLDiscoveredSource(base.TestCase):
def make_discovered_source(self, url=None):
if url is None:
url = self.factory.make_url()
return php_pecl.PHPPECLDiscoveredSource([url])

def test_repr(self):
url = self.factory.make_url()
ds = self.make_discovered_source(url)
self.assertEqual(url, repr(ds))

def test_make_archive(self):
ds = self.make_discovered_source()
self.assertEqual(ds.make_archive, ds.remote_url_is_archive)
3 changes: 2 additions & 1 deletion soufi/tests/test_factory.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) 2021-2023 Cisco Systems, Inc. and its affiliates
# Copyright (c) 2021-2024 Cisco Systems, Inc. and its affiliates
# All rights reserved.

from unittest.mock import MagicMock
Expand Down Expand Up @@ -36,6 +36,7 @@ def test_supported_types(self):
"java",
"npm",
"photon",
"phppecl",
"python",
"rhel",
"ubuntu",
Expand Down

0 comments on commit 800ba93

Please sign in to comment.