Skip to content

Make module loading lazy #162

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Aug 1, 2018
Merged

Make module loading lazy #162

merged 19 commits into from
Aug 1, 2018

Conversation

tkf
Copy link
Member

@tkf tkf commented May 15, 2018

@stevengj suggested to make JuliaModule lazy: #159 (comment)

Here is my implementation for that. I actually added some more changes on top that to solve relevant issues. If some components of the changes have to be removed, let me know. Those components are implemented roughly in the following order:

Basing on CI fix #149

First of all this PR is based on #149 to run CI. Let me know if I need to rebase once #149 is merged.

(Edited: now rebased)

Lazy module member loading

Julia.eval is called via JuliaModule.__getattr__ so that Julia object is brought to Python world only when it is requested.

Implemented in dc40166 and finished by 843b519

Julia's interpreter global namespace as a module julia.Main

In addition to usual JuliaModule, the module class JuliaMainModule of julia.Main has an implementation of __setattr__ so that you can send Python object to Julia by just setting it:

>>> from julia import Main
>>> Main.xs = [1, 2, 3]

Since it supersede core.Julia, I suggest to deprecate accessing Julia values via core.Julia().<name>: b30af45

Automatically initialize Julia with from julia import ...

This makes from julia import <Julia module> works without the initial setup. Note that a custom setup can still be done by calling julia.Julia with appropriate arguments before trying to import Julia modules:
51997c0

See also the document on the new API:
https://github.com/tkf/pyjulia/blob/lazy-module/README.md#usage

julia/core.py Outdated
split_path = module_path.split(".")
is_base = split_path[-1] == "Base"
recur_module = split_path[-1] == split_path[-2]
if not is_base and not recur_module:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure what is the best way to handle submodules. Is there an easy way to check if some module is a submodule of other module in Julia? If not, it's probably better to let user explicitly import submodules (i.e., julia.Module1.Module2 is an AttributeError until the user does import julia.Module1.Module2)?

@tkf
Copy link
Member Author

tkf commented May 15, 2018

In the current master, we have "jl".join(name) in JuliaModuleLoader when Julia name is a Python keyword:

pyjulia/julia/core.py

Lines 77 to 88 in 7ebad22

names = self.julia.eval("names({}, true, false)".format(juliapath))
for name in names:
if (ismacro(name) or
isoperator(name) or
isprotected(name) or
notascii(name)):
continue
attrname = name
if name.endswith("!"):
attrname = name.replace("!", "_b")
if keyword.iskeyword(name):
attrname = "jl".join(name)

What was the intention of this code? I mean, "jl".join("with") evaluates to 'wjlijltjlh' which is not a very human-friendly name.

Was it probably not by intention? Was it maybe intended to be "jl" + name or something?

At the moment, this PR has no code for Python keyword handling. We can add something like:

if name.startswith("jl_"):
    try:
        return self.__try_getattr(name[len("jl_"):])
    except AttributeError:
        pass

@tkf tkf force-pushed the lazy-module branch 4 times, most recently from d8c6bb8 to d057d62 Compare May 16, 2018 02:03
def test_import_without_setup(self):
command = [sys.executable, '-c', 'from julia import Base']
print('Executing:', *command)
subprocess.check_call(command, env=_orig_env)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

env=_orig_env probably is required to fix https://travis-ci.org/JuliaPy/pyjulia/jobs/379514563#L298

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it looks like it fixes the failure: https://travis-ci.org/JuliaPy/pyjulia/builds/379524930

@rdeits
Copy link
Contributor

rdeits commented Jun 20, 2018

CI is now fixed, so you might want to rebase this PR

@tkf
Copy link
Member Author

tkf commented Jun 20, 2018

Thanks. Rebased.

julia/core.py Outdated
def __try_getattr(self, name):
juliapath = remove_prefix(self.__name__, "julia.")
try:
module_path = ".".join((juliapath, name))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe qualified_name or something? Calling this a "path" is a bit confusing.

julia/core.py Outdated
return self.__loader__.load_module(newpath)
return self._julia.eval(module_path)
except JuliaError:
if isafunction(self._julia, name, mod_name=juliapath):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused by this isafunction check. Didn't it already try self._julia.eval(module_path) for all objects that are not modules, including functions? And if that threw an error, shouldn't we rethrow it?

julia/core.py Outdated

def __try_getattr(self, name):
juliapath = remove_prefix(self.__name__, "julia.")
try:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

instead of this try block, maybe we should try calling isdefined(module, :name), throwing an AttributeError if it returns false, and letting any JuliaError that is thrown fall through to the caller.

@tkf
Copy link
Member Author

tkf commented Jun 22, 2018

@stevengj Thanks for the code review!

Some additional changes:

  • I moved DeprecationWarning and compatibility layer to julia.core.LegacyJulia and expose it as julia.Julia. It is not strictly necessary but it was fragile to have julia.Julia.__getattr__ forwarded to juila.Main.__getattr__ as it created infinite recursion (resulting in segfaults) when I introduced some bug while editing the code. Alternatively, I can separate the PR for the new API if you want to focus on reviewing the lazy loader.

  • I removed the import from isamodule. Without this change, trying to access undefined variables such as julia.Base.__path__ produces a warning from Julia before JuliaModule.__getattr__ raising AttributeError, since now isamodule is called via JuliaModule.__getattr__.

@@ -140,8 +140,6 @@ class JuliaImporter(object):

# find_module was deprecated in v3.4
def find_module(self, fullname, path=None):
if path is None:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like this code was added in fa157b1 and carried around as-is.

Probably just a remnant of experimental code?

Copy link

@kmsquire kmsquire left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made a few suggestions regarding wording changes.

I like the changes, especially being able to import Julia modules without setup.

However, I find that when I run ipython3 from the command line, I get a segfault during tab completion of anything in a Julia module:

# pip3 install ipython # if necessary
> ipython
Python 3.6.5 (default, Jun 17 2018, 12:13:06)
Type 'copyright', 'credits' or 'license' for more information
IPython 6.4.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: import julia

In [2]: from julia import Base

In [3]: Base.<tab>[1]    79420 segmentation fault  ipython

master does not have this problem. Using regular python3 (instead of ipython3) does not have this problem.

README.md Outdated
Main.xs = [1, 2, 3]
```

so that, e.g., it can be evaluated at Julia side using Julia syntax:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps change the wording to:
which allows it to be accessed directly from Julia code, e.g.,

(While one can do this, I think it's better to pass values via function call arguments, rather than rely on values existing in global scope.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I totally agree that communication-by-sharing is a terrible idea and using this feature in Python modules should be forbidden :) But I was thinking in terms of interactive usage, mostly via %julia. In that case, passing around code via global namespace is pretty useful.

README.md Outdated
### Low-level interface

If you need a custom setup for `pyjulia`, it must be done *before* any
imports of Julia modules. For example, to use Julia interpreter at

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion for some (minor) wording changes:

If you need a custom setup for `pyjulia`, it must be done *before* 
importing any Julia modules.  For example, to use the Julia interpreter at

README.md Outdated
### High-level interface

To call a Julia function in Julia module, import the Julia module (say
`Base`) by:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion for (minor) wording changes:

To call a Julia function in a Julia module, import the Julia module (say
`Base`) with

@tkf
Copy link
Member Author

tkf commented Jul 31, 2018

Hi @kmsquire, thanks a lot for correcting my English!

BTW, it's interesting that you find that IPython tab completion works in pyjulia master. I thought, in theory, pyjulia would never work with IPython < 7.0 since the tab-completion is done in different thread #132 (comment) and pyjulia (or rather libjulia, I guess) is not thread-safe. I actually find out that IPython master (= 7.x) already works in single-thread just now. I have to check it...

@tkf
Copy link
Member Author

tkf commented Jul 31, 2018

Test failure is due to CI setting. See #171.

@stevengj
Copy link
Member

Is this ready to merge? I haven't really been keeping up with pyjulia…

@tkf
Copy link
Member Author

tkf commented Jul 31, 2018

I think so. I mean, I used this branch for a few weeks just after creating it and I don't remember encountering any regressions. After that, the only change was in README. I'm not planning to add anything more to this PR.

tkf added 7 commits July 31, 2018 15:08
It adds an easier way to set variables in Julia's global namespace:

>>> from julia import Main
>>> Main.xs = [1, 2, 3]
This makes `from julia import <Julia module>` works without the initial
setup.  Note that a custom setup can still be done by calling
`julia.Julia` with appropriate arguments *before* trying to import Julia
modules.

closes JuliaPy#39, JuliaPy#79
@tkf
Copy link
Member Author

tkf commented Jul 31, 2018

Rebased onto master. Travis and AppVeyor are all green now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants