Repository: mesonbuild/meson

Issue:
Dependencies on python modules
For mesa I need to check if the mako module is installed (and of the proper version) for the python version we're using (python2 at this point :/ ).

I'm interested in adding the feature to meson, but I want to know whether the preferred implementation would be to writing a dependency type (maybe that took a python prog as an argument?), or to add a method to the python module (obviously for the latter we'd need a python2 module).


Discussion:
The Autotools build looks for python2, but I looked at a few scripts and they seemed to be compatible with both python2 and python3? Maybe the ones that aren't can be ported to be Py2/Py3 compatible? They don't seem to be doing anything special that would prevent that.

I think for the Meson build it makes sense to require Python3 since that's what Meson is also written in. That will make your job easier because then you can add module searching to the python3 module.
I'm working on porting to python 3 (I know, I'm juggling 4+ major projects at a time, lol), the problem is that some of the scripts are incredibly complicated and have deep rooted python2 assumptions built into them. There's also a *lot* of code generation is mesa.

Anyway, I think that the answer I'm looking for is that adding a `has_module` method to the python module would be acceptable.
`has_module` would make it more consistent with, for instance, compiler checks. But yes, we'd happily accept a python2 module if you submit one. Other people have also asked about it in the past.
Could we also link up `has_module` with the `dependency` method? It's a bit confusing/inconsistent if we end up with having to detect Python dependencies separately from dependencies for other languages (using `has_module` instead of just `dependency` possibly with `method: python`).
`has_module` makes the python depdendency feel more like a compiler object, like `c.has_library`. I haven't paid too much attention to the changes in the python module, but dependencies already have  `modules` argument, llvm and qt use that.

Resolving patch:
diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py
--- a/mesonbuild/modules/python.py
+++ b/mesonbuild/modules/python.py
@@ -15,6 +15,7 @@
 import os
 import json
 import shutil
+import typing
 
 from pathlib import Path
 from .. import mesonlib
@@ -491,11 +492,15 @@ def _check_version(self, name_or_path, version):
         return True
 
     @FeatureNewKwargs('python.find_installation', '0.49.0', ['disabler'])
+    @FeatureNewKwargs('python.find_installation', '0.51.0', ['modules'])
     @disablerIfNotFound
-    @permittedKwargs(['required'])
+    @permittedKwargs({'required', 'modules'})
     def find_installation(self, interpreter, state, args, kwargs):
         feature_check = FeatureNew('Passing "feature" option to find_installation', '0.48.0')
         disabled, required, feature = extract_required_kwarg(kwargs, state.subproject, feature_check)
+        want_modules = mesonlib.extract_as_list(kwargs, 'modules')  # type: typing.List[str]
+        found_modules = []    # type: typing.List[str]
+        missing_modules = []  # type: typing.List[str]
 
         if len(args) > 1:
             raise InvalidArguments('find_installation takes zero or one positional argument.')
@@ -511,7 +516,7 @@ def find_installation(self, interpreter, state, args, kwargs):
             return ExternalProgramHolder(NonExistingExternalProgram())
 
         if not name_or_path:
-            python = ExternalProgram('python3', mesonlib.python_command)
+            python = ExternalProgram('python3', mesonlib.python_command, silent=True)
         else:
             python = ExternalProgram.from_entry('python3', name_or_path)
 
@@ -528,13 +533,38 @@ def find_installation(self, interpreter, state, args, kwargs):
             if not python.found() and name_or_path in ['python2', 'python3']:
                 python = ExternalProgram('python', silent=True)
 
-            mlog.log('Program', python.name, 'found:',
-                     *[mlog.green('YES'), '({})'.format(' '.join(python.command))] if python.found() else [mlog.red('NO')])
+        if python.found() and want_modules:
+            for mod in want_modules:
+                p, out, err = mesonlib.Popen_safe(
+                    python.command +
+                    ['-c', 'import {0}'.format(mod)])
+                if p.returncode != 0:
+                    missing_modules.append(mod)
+                else:
+                    found_modules.append(mod)
+
+        msg = ['Program', python.name]
+        if want_modules:
+            msg.append('({})'.format(', '.join(want_modules)))
+        msg.append('found:')
+        if python.found() and not missing_modules:
+            msg.extend([mlog.green('YES'), '({})'.format(' '.join(python.command))])
+        else:
+            msg.append(mlog.red('NO'))
+        if found_modules:
+            msg.append('modules:')
+            msg.append(', '.join(found_modules))
+
+        mlog.log(*msg)
 
         if not python.found():
             if required:
                 raise mesonlib.MesonException('{} not found'.format(name_or_path or 'python'))
             res = ExternalProgramHolder(NonExistingExternalProgram())
+        elif missing_modules:
+            if required:
+                raise mesonlib.MesonException('{} is missing modules: {}'.format(name_or_path or 'python', ', '.join(missing_modules)))
+            res = ExternalProgramHolder(NonExistingExternalProgram())
         else:
             # Sanity check, we expect to have something that at least quacks in tune
             try:


Test patch:
diff --git a/test cases/python/5 modules kwarg/meson.build b/test cases/python/5 modules kwarg/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/python/5 modules kwarg/meson.build	
@@ -0,0 +1,7 @@
+project('python kwarg')
+
+py = import('python')
+prog_python = py.find_installation('python3', modules : ['setuptools'])
+assert(prog_python.found() == true, 'python not found when should be')
+prog_python = py.find_installation('python3', modules : ['thisbetternotexistmod'], required : false)
+assert(prog_python.found() == false, 'python not found but reported as found')