Nested Python Modules


I am trying to execute the following code:

py_call(selenium:webdriver:'Chrome'(), Browser).

But I get the following error:

ERROR:   module 'selenium' has no attribute 'webdriver'

I want to mimic the following Python code:

from selenium import webdriver

browser = webdriver.Chrome()

But Janus does not seem to support access to nested Python modules.

Any help is appreciated,

Good question. I still do not fully understand what is going on. I can work around it using this code:

:- use_module(library(janus)).
:- use_module(library(strings)).

:- py_module(selenium,
              | from selenium import webdriver

chrome(Chrome) :-
    py_call(selenium:webdriver:'Chrome'(), Chrome).

If some Python person knows what really happens and how to load a module from a package (as I think that is actually what happens) through the C API, please share.

Replace the colons “:” by dots “.” in the module name:

module = importlib.import_module(name)

Maybe there is no direct C-Call, and you need reflection?
Don’t know, but from the documentation:

importlib.import_module(name, package=None )
The name argument specifies what module to import in
absolute or relative terms (e.g. either pkg.mod or ..mod).
importlib — The implementation of import — Python 3.12.2 documentation

The API call has also a named formal parameter package,
to specify the anchor to resolve relative modules.

But in either case you have to also see to it that everything
is in the Python search path. Which you can dynamically
modify via sys.path.

Edit 24.03.2024
But then a module might contain members such as classes,
functions and fields. Which you can access once you have the
loaded module handle, by navigating the module content.

For example I have the convention that my native modules
are initialized via this here, once I have the module handle:


So I am expecting a zero-ary function member by the name main.
In Python class constructors and zero-ary functions are not
easy to distinguish they have the same call markup, i.e. Chrome().

Also members can be nested, like functions can have subfunctions,
and classes can have subclasses. Maybe there is even a feature
like in Java, where it makes sense to access submembers,

when they are static. And then there is some stuff going on in Python
with regular packages versus namespace packages, which I am a little
bit ignorant about.

I tried that, but I probably made some other mistake and misinterpreted the error :frowning: This works:

?- py_call('selenium.webdriver':'Chrome'(), Chrome).
Chrome = <py_WebDriver>(0x7ff30e9cc2e0).

I don’t really like this. Possibly we should have a py_import/1, so we can write

?- py_import(selenium:webdriver).
?- py_call(webdriver:'Chrome'(), Chrome)?



To have nested modules accessed by their unnested name,
in the Python programming language itself, you have
to do something along:

import nova.runtime as runtime

You can then use in the non-nested module that had the import:

Without the rename, I believe you have still to use the nested name like
here in the Python programming language itself.
Further I think the reflection API importlib.import_module() alone

doesn’t provide some rename. It doesn’t have some way to specifie an
“as” part and doesn’t have a default “as” part, most likely it only returns
a possibly previously cached module handle without even registering

it in the caller module by some name. In the wild you also find often
that people perform a proper rename by the import statement of the
Python programming language, like this is seen here:

import numpy as np 
a = np.arange(15).reshape(3, 5)

Ok, Added py_import/2. Copying the docs. Note this is tentative. As we try to keep the Janus interface compatible with XSB this may change if the XSB thinks this predicate is a good idea, but wants the defaults different. It may be dropped in case it is proven redundant (but I don’t think it is).

%!  py_import(+Spec, +Options) is det.
%   Import a Python module.  Janus   imports  modules automatically when
%   referred in py_call/2 and  related   predicates.  Importing a module
%   implies  the  module  is  loaded   using  Python's  ``__import__()``
%   built-in and added to a table  that   maps  Prolog atoms to imported
%   modules. This predicate explicitly imports a module and allows it to
%   be associated with a different  name.   This  is  useful for loading
%   _nested modules_, i.e., a specific module   from a Python package as
%   well as for  avoiding  conflicts.  For   example,  with  the  Python
%   `selenium` package installed, we can do in Python:
%       >>> from selenium import webdriver
%       >>> browser = webdriver.Chrome()
%   Without this predicate, we can do
%       ?- py_call('selenium.webdriver':'Chrome'(), Chrome).
%   For a single call this is  fine,   but  for making multiple calls it
%   gets cumbersome.  With this predicate we can write this.
%       ?- py_import('selenium.webdriver', []).
%       ?- py_call(webdriver:'Chrome'(), Chrome).
%   By default, the imported module  is   associated  to an atom created
%   from the last segment of the dotted   name. Below we use an explicit
%   name.
%       ?- py_import('selenium.webdriver', [as(browser)]).
%       ?- py_call(browser:'Chrome'(), Chrome).
%   @error  permission_error(import_as,  py_module,  As)   if  there  is
%   already a module associated with As.