1

I have two versions of the same Python package. I need from a module in a subpackage in the current version to be able to call a function inside the old version of the package (which copied itself in the past)

Where I am now:

now/
  package/
    __init__.py
    subpackage/
      __init__.py
      module.py -> "import package.subpackage.... <HERE>"
    subpackage2/
      ...
    ...

The old version:

past/
  package/
    __init__.py
    subpackage/
      __init__.py
      module.py -> "import package.subpackage; from . import module2; .... def f(x) ..."
      module2.py
    subpackage2/
      ...
    ...

I need to import in <HERE> the "old" f and run it.

Ideally

  • the function f should live its life inside the old package without knowing anything about the new version of the package
  • the module in the new package should call it, let it live its life, get the results and then forget altogether about the existence of the old package (so calling "import package.subpackage2" after letting f do her thing should run the "new" version)
  • doing that should not be terribly complex

The underlying idea is to improve reproducibility by saving the code that I used for some task along with the output data, and then being able to run parts of it.

Sadly, I understood this is not a simple task with Python 3, so I am prepared to accept some sort of compromise. I am prepared to accept, for example that after running the old f(x) the name package in the "new" code will be bound to the old.

EDIT

I tried in two ways using importlib. The idea was to create an object mod and then doing f = getattr(mod, "f"), but it doesn't work

  1. Changing sys.path to ['.../past/package/subpackage'] and then calling importlib.import_module('package.subpackage.module') . The problem is that it will load the one in "now" even with the changed sys.path, probably because the name package is already in sys.modules
  2. spec = importlib.util.spec_from_file_location("module", "path..to..past..module.py")) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) In that case relative imports (from . import module2.py) won't work, giving the error "attempted relative import with no known parent package"
Fiat Lux
  • 227
  • 2
  • 7
  • 1
    that's generally not recommended as old versions of packages generally have inter-dependencies on other packages and the version of python they're being run against. I'd suggest something like pickling parameters, spinning up a new interpreter in a different `virtualenv`, then unpicking and running in that – Sam Mason Oct 15 '18 at 17:45
  • Why isn't regular import working for you in this case? I don't think there's any bulletproof way to do this and guarantee there will be no import namespace clashes between the two versions of the package. I don't know of any library that is designed to coexist with other versions of the same library. – Håken Lid Oct 15 '18 at 17:50
  • I don't understand: Are you trying to use two versions of `module.py` at the same time? I'm assuming the `def x(x):` function definition is instead it. – martineau Oct 15 '18 at 17:50
  • This is a _bad idea_ ™. You should not use multiple versions of the same library at the same time. – g.d.d.c Oct 15 '18 at 17:53
  • @HåkenLid because it won't import `package.subpackage.module` from the old directory even if I change `sys.path` if I have already done `import package.subpackage2` in the new directory. I believe it is connected to `sys.modules` containing `'package'` – Fiat Lux Oct 15 '18 at 18:25
  • @martineau basically yes. Ideally there will be an `f` and an `old_f`, both working in `` – Fiat Lux Oct 15 '18 at 18:58
  • @SamMason I understand, the reason is that I am doing some calculations in the package and the function `f` is acting as a parser for the final data. Ideally I would like to back up the code and be able to programmatically read the data even if the format changes. Pickling would be a bit overkill, I can safely assume for now that libraries did not change – Fiat Lux Oct 15 '18 at 19:05
  • Fiat: In order to have two `f` functions available at the same time, their definitions would have to be in two separate `.py` files. If those files are in the same directory, you'll have to give them distinct names—they can't both be in a `module.py`. Is that what the `module2.py` you added to your question is about? Once they're in separate files, you can just `from module2 import f as old_f`. Alternatively, you could just `import module2` and use `module2.f()` to refer to the old one. – martineau Oct 15 '18 at 19:05
  • @martineau sorry I'll try to clarify the question. I need to use the `f` from `past/package/subpackage/module.py` inside `now/package/subpackage/module.py`. Files are different, but it seems to be difficult to import a module inside a package that has the same name as a package I have already loaded – Fiat Lux Oct 15 '18 at 19:08
  • @FiatLux I think you'll keep bumping up against some fundamental design assumptions baked pretty deep into the language. maybe if you could turn this into a MWE (I'm thinking it could write it's own module/source files) you might get people interested enough to help – Sam Mason Oct 16 '18 at 10:35
  • @FiatLux: I've updated my answer with how you could do this. – Lie Ryan Oct 17 '18 at 14:03

1 Answers1

1

There is one way this could work quite simply, but you will have to make a few modifications to your old package.

You can simply create a file in now/package/old/__init__.py containing:

__path__ = ['/absolute/path/to/old/package']

In the new package, you can then do:

from package.old.package.subpackage.module import f as old_f

The catch here is that the old package tries to import its own packages using absolute import, it is going to load stuff from the new packages instead. So the old package will have to only use relative imports when importing stuffs from its own package or you'll have to prepend package.old to all absolute imports that the old package was doing.

If you are fine with modifying the old packages in this way, then that should be fine. If that limitation would not work for you, then read on.

If you are really, really sure that for some reasons don't want to modify the old packages. Then let's do some black magic, you'd want to replace builtins.__import__ with your own version that returns different modules depending on who is doing the importing. You can figure out who is doing the importing by inspecting the call stack.

For example, this is how you might do it (tested on Python 3.6):

import builtins
import inspect
import package.old

old_package_path = package.old.__path__[0]

OUR_PACKAGE_NAME = 'package'
OUR_PACKAGE_NAME_WITH_DOT = OUR_PACKAGE_NAME + '.'


def import_module(name, globs=None, locs=None, fromlist=(), level=0):
    # only intercept imports for our own package from our old module
    if not name.startswith(OUR_PACKAGE_NAME_WITH_DOT) or \
            not inspect.stack()[1].filename.startswith(old_package_path):
        return real_import(name, globs, locs, fromlist, level)

    new_name = OUR_PACKAGE_NAME + '.old.' + name[len(OUR_PACKAGE_NAME_WITH_DOT):]
    mod = real_import(new_name, globs, locs, fromlist, level)
    return mod.old

# save the original __import__ since we'll need it to do the actual import
real_import = builtins.__import__
builtins.__import__ = import_module

builtins.__import__ gets called on any import statements encountered by the interpreter, and the call is not cached so you can return different things every time it is called even when they use the same name.


The following is my old answer, here for historical purpose only

I don't quite get what you're trying to do, but this is likely possible to do in Python 3 by using importlib.

You would just create a module loader that loads your module from an explicit filepath.

There's also an invalidate_caches() and reload() function which may be useful, though you may not need them.

Lie Ryan
  • 62,238
  • 13
  • 100
  • 144