Hacker News new | ask | show | jobs
by gorgoiler 667 days ago
For fun, you could add this to Python and I think it would it cover a lot of edge cases?

You would need:

A function v_tree_install(spec) which installs a versioned pypi package like “foo=3.2” and all its dependencies in its own tree, rather than in site-packages.

Another pair of functions v_import and v_from_import to wrap importlib with a name, version, and symbols. These functions know how to find the versioned package in its special tree and push that tree to sys.path before starting the import.

To cover the case for when the imported code has dynamic imports you could also wrap any callable code (functions, classes) with a wrapper that also does the sys.push/pop before/after each call.

You then replace third party imports in your code with calls assigning to symbols in your module:

  # import foo
  foo = v_import(“foo==3.2”)

  # from foo import bar, baz as q
  bar, q = v_from_import(
    “foo>=3.3”,
    “bar”,
    “baz”,
  )
Finally, provide a function (or CLI tool) to statically scan your code looking for v_import and calling v_tree_install ahead of time. Or just let v_import do it.

Edit: …and you’d need to edit the sys.modules cache too, or purge it after each “clever” import?

3 comments

I can’t see how it would ever be possible with Python to do this.

You depend on two packages, each with a function that returns a “requests.Request” object. These packages depend on different versions of “requests”.

How would you implement “isinstance(return_value, requests.Request)” on each of these calls?

Or, the indirect case of this: catching a “requests.HttpException” from each of these calls?

Importing the right thing isn’t hard, but doing things with it is the hard bit.

I see what you mean. They might as well be two different types:

  from m1 import T1
  from m2 import T2
  from m3 import f

  x = f()
  assert isinstance(x, T1, T2)
Perhaps you could have a v_import that imported all versions of a symbol used throughout your project?

  Ts = v_from_import_all(
    “foo”,
    “T”,
  )
  assert isinstance(x, *Ts)
For static analysis, your type checker could understand what v_import does, how it works, and which symbols will be actually be there at runtime but yes, it’s starting to seem extremely complicated!

What you do with the return_value defines the behaviour you expect from it so to that extent you can rely on that instead of using isinstance:

  x: Union[T1, T2] = f()
  print(x.foo() ** 12.3)
Perhaps some function could build that Union type for you? It would be a pain to make it by hand if you had 50x different third-party dependencies each pulling in a slightly different requests (but which as far as you are concerned all return some small part of that package that are all compatible.)

If you’re importing a module to use it in some way you’re also declaring some kind of version dependency / compatability on it too, so that’s another thing your static analysis could check for you. That would actually be incredibly useful:

1/ Do your dependencies import an older version of requests than you do?

2/ Does it matter, and why? (eg x.foo() only exists in version 4.5 onwards, but m1 imports 4.4.)

> They might as well be two different types

The problem is they are different types, which has huge downstream impacts. Not least of all would be subclassing.

The static analysis you’ve described could work in simple and trivial cases, but the problem you’re now trying to solve is “what concrete type is this object”, and in a dynamic language like Python this can only be fully and 100% determined at runtime.

Mypy and the like do a good job, but often rely on protocols rather than concrete classes. There’s also no impact if the types are wrong or ignored, whereas for this a typing mismatch becomes a subtle runtime issue.

You might be able to do this transparently with a [MetaPathFinder](https://docs.python.org/3/library/importlib.html#importlib.a...), the only trickyness would be replacing the lookup in sys.modules which I don't think has has an official interface.
I have though about this a bunch (and have been annoyed by it a bunch).

But the main issue here is somewhat designed around a "scripts and folder of scripts from a package" design principle while such a loading system would fundamentally need to always work in terms of packages. E.g. you wouldn't execute `main.py` but `package:main`. (Through this is already the direction a bunch of tooling moved to, e.g. poetry scripts, some of the WSGI and especially more modern ASGI implementations etc.)

Another issue is that rust can reliable detect type collisions of the same type of two different versions and force you to fix them.

With a lot of struct type annotations on python and tooling like mypy this might be possible (with many limitations) but as of today it in practice likely will not be caught. Sometimes that is what you want (ducktyping happens to work). But for any of the reflection/inspection heavy python library this is a recipe for quite obscure errors somewhere in not so obvious inspection/auto generation/metaclass related magic code. Python can't, escept it can

Anyway technically it's possible, you can put a version into __qualname__, and mess with the import system enough to allow imports to be contextual based on the manifest of the module they come from. (Through you probably would not be fully standard conform python, but we are speaking about dynamic patching pythons import system, there is nothing standard about it)