Author: Armin Rigo <[email protected]>
Branch: 
Changeset: r3205:bafc95c0591a
Date: 2019-01-31 12:14 +0100
http://bitbucket.org/cffi/cffi/changeset/bafc95c0591a/

Log:    Tweaks to the pkgconfig support

diff --git a/cffi/__init__.py b/cffi/__init__.py
--- a/cffi/__init__.py
+++ b/cffi/__init__.py
@@ -3,6 +3,7 @@
 
 from .api import FFI
 from .error import CDefError, FFIError, VerificationError, VerificationMissing
+from .error import PkgConfigError
 
 __version__ = "1.12.0"
 __version_info__ = (1, 12, 0)
diff --git a/cffi/api.py b/cffi/api.py
--- a/cffi/api.py
+++ b/cffi/api.py
@@ -2,7 +2,6 @@
 from .lock import allocate_lock
 from .error import CDefError
 from . import model
-from . import pkgconfig
 
 try:
     callable
@@ -642,8 +641,11 @@
             raise ValueError("'module_name' must not contain '/': use a dotted 
"
                              "name to make a 'package.module' location")
         if "pkgconfig" in kwds:
-            pkgconfig.merge_flags(kwds, pkgconfig.flags(kwds["pkgconfig"]))
-            del kwds["pkgconfig"]
+            from . import pkgconfig
+            libs = kwds.pop("pkgconfig")
+            if not isinstance(libs, (list, tuple)):
+                libs = [libs]
+            pkgconfig.merge_flags(kwds, pkgconfig.flags_from_pkgconfig(libs))
         self._assigned_source = (str(module_name), source,
                                  source_extension, kwds)
 
diff --git a/cffi/error.py b/cffi/error.py
--- a/cffi/error.py
+++ b/cffi/error.py
@@ -1,8 +1,9 @@
 
 class FFIError(Exception):
-    pass
+    __module__ = 'cffi'
 
 class CDefError(Exception):
+    __module__ = 'cffi'
     def __str__(self):
         try:
             current_decl = self.args[1]
@@ -16,15 +17,15 @@
 class VerificationError(Exception):
     """ An error raised when verification fails
     """
+    __module__ = 'cffi'
 
 class VerificationMissing(Exception):
     """ An error raised when incomplete structures are passed into
     cdef, but no verification has been done
     """
+    __module__ = 'cffi'
 
 class PkgConfigError(Exception):
-    """ An error raised for all pkg-config related errors
-    except version mismatch"""
-
-class PkgConfigModuleVersionNotFound(Exception):
-    """ An error raised when requested version was not found"""
+    """ An error raised for missing modules in pkg-config
+    """
+    __module__ = 'cffi'
diff --git a/cffi/pkgconfig.py b/cffi/pkgconfig.py
--- a/cffi/pkgconfig.py
+++ b/cffi/pkgconfig.py
@@ -1,51 +1,63 @@
 # pkg-config, https://www.freedesktop.org/wiki/Software/pkg-config/ 
integration for cffi
-import subprocess
-import sys
-import re
+import sys, os, subprocess
 
-from .error import PkgConfigModuleVersionNotFound
 from .error import PkgConfigError
 
+
 def merge_flags(cfg1, cfg2):
     """Merge values from cffi config flags cfg2 to cf1
 
     Example:
-        merge_flags({"libraries": ["one"]}, {"libraries": "two"})
-        {"libraries}" : ["one", "two"]}
+        merge_flags({"libraries": ["one"]}, {"libraries": ["two"]})
+        {"libraries": ["one", "two"]}
     """
     for key, value in cfg2.items():
-        if not key in cfg1:
-            cfg1 [key] = value
+        if key not in cfg1:
+            cfg1[key] = value
         else:
-            cfg1 [key].extend(value)
+            if not isinstance(cfg1[key], list):
+                raise TypeError("cfg1[%r] should be a list of strings" % 
(key,))
+            if not isinstance(value, list):
+                raise TypeError("cfg2[%r] should be a list of strings" % 
(key,))
+            cfg1[key].extend(value)
     return cfg1
 
 
-def call(libname, flag):
-    """Calls pkg-config and returing the output if found
+def call(libname, flag, encoding=sys.getfilesystemencoding()):
+    """Calls pkg-config and returns the output if found
     """
     a = ["pkg-config", "--print-errors"]
     a.append(flag)
     a.append(libname)
-    pc = None
     try:
         pc = subprocess.Popen(a, stdout=subprocess.PIPE, 
stderr=subprocess.PIPE)
-    except FileNotFoundError:
-        pass
-    if pc is None:
-        raise PkgConfigError("pkg-config was not found on this system")
-    
+    except EnvironmentError as e:
+        raise PkgConfigError("cannot run pkg-config: %s" % (str(e).strip(),))
+
     bout, berr = pc.communicate()
-    if berr is not None:
-        err = berr.decode(sys.getfilesystemencoding())
-        if re.search("Requested '.*' but version of ", err, re.MULTILINE) is 
not None:
-            raise PkgConfigModuleVersionNotFound(err)
-        else:
-            PkgConfigError(err)
+    if pc.returncode != 0:
+        try:
+            berr = berr.decode(encoding)
+        except Exception:
+            pass
+        raise PkgConfigError(berr.strip())
+
+    if sys.version_info >= (3,) and not isinstance(bout, str):   # Python 3.x
+        try:
+            bout = bout.decode(encoding)
+        except UnicodeDecodeError:
+            raise PkgConfigError("pkg-config %s %s returned bytes that cannot "
+                                 "be decoded with encoding %r:\n%r" %
+                                 (flag, libname, encoding, bout))
+
+    if os.altsep != '\\' and '\\' in bout:
+        raise PkgConfigError("pkg-config %s %s returned an unsupported "
+                             "backslash-escaped output:\n%r" %
+                             (flag, libname, bout))
     return bout
 
 
-def flags(libs):
+def flags_from_pkgconfig(libs):
     r"""Return compiler line flags for FFI.set_source based on pkg-config 
output
 
     Usage
@@ -57,49 +69,53 @@
     extra_link_args are extended with an output of pkg-config for libfoo and
     libbar.
 
-    Raises
-    * PkgConfigModuleVersionNotFound if requested version does not match
-    * PkgConfigError for all other errors
+    Raises PkgConfigError in case the pkg-config call fails.
     """
 
-    subprocess.check_output(["pkg-config", "--version"])
+    def get_include_dirs(string):
+        return [x[2:] for x in string.split() if x.startswith("-I")]
 
-    # make API great again!
-    if isinstance(libs, (str, bytes)):
-        libs = (libs, )
-    
-    # drop starting -I -L -l from cflags
-    def dropILl(string):
-        def _dropILl(string):
-            if string.startswith("-I") or string.startswith("-L") or 
string.startswith("-l"):
-                return string [2:]
-        return [_dropILl(x) for x in string.split()]
+    def get_library_dirs(string):
+        return [x[2:] for x in string.split() if x.startswith("-L")]
 
-    # convert -Dfoo=bar to list of tuples [("foo", "bar")] expected by cffi
-    def macros(string):
-        def _macros(string):
-            return tuple(string [2:].split("=", 2))
-        return [_macros(x) for x in string.split() if x.startswith("-D")]
+    def get_libraries(string):
+        return [x[2:] for x in string.split() if x.startswith("-l")]
 
-    def drop_macros(string):
-        return [x for x in string.split() if not x.startswith("-D")]
+    # convert -Dfoo=bar to list of tuples [("foo", "bar")] expected by 
distutils
+    def get_macros(string):
+        def _macro(x):
+            x = x[2:]    # drop "-D"
+            if '=' in x:
+                return tuple(x.split("=", 1))  # "-Dfoo=bar" => ("foo", "bar")
+            else:
+                return (x, None)               # "-Dfoo" => ("foo", None)
+        return [_macro(x) for x in string.split() if x.startswith("-D")]
+
+    def get_other_cflags(string):
+        return [x for x in string.split() if not x.startswith("-I") and
+                                             not x.startswith("-D")]
+
+    def get_other_libs(string):
+        return [x for x in string.split() if not x.startswith("-L") and
+                                             not x.startswith("-l")]
 
     # return kwargs for given libname
     def kwargs(libname):
         fse = sys.getfilesystemencoding()
+        all_cflags = call(libname, "--cflags")
+        all_libs = call(libname, "--libs")
         return {
-                "include_dirs" : dropILl(call(libname, 
"--cflags-only-I").decode(fse)),
-                "library_dirs" : dropILl(call(libname, 
"--libs-only-L").decode(fse)),
-                "libraries" : dropILl(call(libname, 
"--libs-only-l").decode(fse)),
-                "define_macros" : macros(call(libname, 
"--cflags-only-other").decode('ascii')),
-                "extra_compile_args" : drop_macros(call(libname, 
"--cflags-only-other").decode('ascii')),
-                "extra_link_args" : call(libname, 
"--libs-only-other").decode('ascii').split()
-                }
+            "include_dirs": get_include_dirs(all_cflags),
+            "library_dirs": get_library_dirs(all_libs),
+            "libraries": get_libraries(all_libs),
+            "define_macros": get_macros(all_cflags),
+            "extra_compile_args": get_other_cflags(all_cflags),
+            "extra_link_args": get_other_libs(all_libs),
+            }
 
     # merge all arguments together
     ret = {}
     for libname in libs:
-        foo = kwargs(libname)
-        merge_flags(ret, foo)
-
+        lib_flags = kwargs(libname)
+        merge_flags(ret, lib_flags)
     return ret
diff --git a/testing/cffi1/test_pkgconfig.py b/testing/cffi1/test_pkgconfig.py
--- a/testing/cffi1/test_pkgconfig.py
+++ b/testing/cffi1/test_pkgconfig.py
@@ -2,24 +2,19 @@
 import subprocess
 import py
 import cffi.pkgconfig as pkgconfig
+from cffi import PkgConfigError
+
 
 def mock_call(libname, flag):
-    assert libname=="python-3.6", "mocked pc function supports python-3.6 
input ONLY"
-
+    assert libname=="foobarbaz"
     flags = {
-        "--cflags-only-I": b"-I/usr/include/python3.6m\n",
-        "--libs-only-L": b"-L/usr/lib64\n",
-        "--libs-only-l": b"-lpython3.6\n",
-        "--cflags-only-other": b"-DCFFI_TEST=1 -O42\n",
-        "--libs-only-other": b"-lm\n",
+        "--cflags": "-I/usr/include/python3.6m -DABCD -DCFFI_TEST=1 -O42\n",
+        "--libs": "-L/usr/lib64 -lpython3.6 -shared\n",
     }
     return flags[flag]
 
-pkgconfig.call = mock_call
-
 
 def test_merge_flags():
-
     d1 = {"ham": [1, 2, 3], "spam" : ["a", "b", "c"], "foo" : []}
     d2 = {"spam" : ["spam", "spam", "spam"], "bar" : ["b", "a", "z"]}
 
@@ -32,12 +27,68 @@
 
 
 def test_pkgconfig():
-    flags = pkgconfig.flags("python-3.6")
+    assert pkgconfig.flags_from_pkgconfig([]) == {}
+
+    saved = pkgconfig.call
+    try:
+        pkgconfig.call = mock_call
+        flags = pkgconfig.flags_from_pkgconfig(["foobarbaz"])
+    finally:
+        pkgconfig.call = saved
     assert flags == {
-        'include_dirs': [u'/usr/include/python3.6m'],
-        'library_dirs': [u'/usr/lib64'],
-        'libraries': [u'python3.6'],
-        'define_macros': [(u'CFFI_TEST', u'1')],
-        'extra_compile_args': [u'-O42'],
-        'extra_link_args': [u'-lm']
+        'include_dirs': ['/usr/include/python3.6m'],
+        'library_dirs': ['/usr/lib64'],
+        'libraries': ['python3.6'],
+        'define_macros': [('ABCD', None), ('CFFI_TEST', '1')],
+        'extra_compile_args': ['-O42'],
+        'extra_link_args': ['-shared']
     }
+
+class mock_subprocess:
+    PIPE = Ellipsis
+    class Popen:
+        def __init__(self, cmd, stdout, stderr):
+            if mock_subprocess.RESULT is None:
+                raise OSError("oops can't run")
+            assert cmd == ['pkg-config', '--print-errors', '--cflags', 
'libfoo']
+        def communicate(self):
+            bout, berr, rc = mock_subprocess.RESULT
+            self.returncode = rc
+            return bout, berr
+
+def test_call():
+    saved = pkgconfig.subprocess
+    try:
+        pkgconfig.subprocess = mock_subprocess
+
+        mock_subprocess.RESULT = None
+        e = py.test.raises(PkgConfigError, pkgconfig.call, "libfoo", 
"--cflags")
+        assert str(e.value) == "cannot run pkg-config: oops can't run"
+
+        mock_subprocess.RESULT = b"", "Foo error!\n", 1
+        e = py.test.raises(PkgConfigError, pkgconfig.call, "libfoo", 
"--cflags")
+        assert str(e.value) == "Foo error!"
+
+        mock_subprocess.RESULT = b"abc\\def\n", "", 0
+        e = py.test.raises(PkgConfigError, pkgconfig.call, "libfoo", 
"--cflags")
+        assert str(e.value).startswith("pkg-config --cflags libfoo returned an 
"
+                                       "unsupported backslash-escaped output:")
+
+        mock_subprocess.RESULT = b"abc def\n", "", 0
+        result = pkgconfig.call("libfoo", "--cflags")
+        assert result == "abc def\n"
+
+        mock_subprocess.RESULT = b"abc def\n", "", 0
+        result = pkgconfig.call("libfoo", "--cflags")
+        assert result == "abc def\n"
+
+        if sys.version_info >= (3,):
+            mock_subprocess.RESULT = b"\xff\n", "", 0
+            e = py.test.raises(PkgConfigError, pkgconfig.call,
+                               "libfoo", "--cflags", encoding="utf-8")
+            assert str(e.value) == (
+                "pkg-config --cflags libfoo returned bytes that cannot be "
+                "decoded with encoding 'utf-8':\nb'\\xff\\n'")
+
+    finally:
+        pkgconfig.subprocess = saved
_______________________________________________
pypy-commit mailing list
[email protected]
https://mail.python.org/mailman/listinfo/pypy-commit

Reply via email to