1

I would like to create a test for a python 3.7+ script called foo-bar (that's the file name, and it has no .py extension):

#!/usr/bin/env python

def foo(bar):
  return bar + 42

if __name__ == '__main__':
    print(foo(1))

How can I load this file by path alone, so that I can test the foo() method? The test should NOT trigger the if main condition.

UPDATE note that this is not about executing the file from the test (i.e. exec('foo-bar')), but rather loading/importing it as a module/resource, allowing the test code to execute foo() on it.

Yuri Astrakhan
  • 8,808
  • 6
  • 63
  • 97
  • 1
    Why do you want to have a Python file without the `.py` extension? – DYZ Mar 10 '20 at 05:09
  • 1) this code already exists, so backwards compat 2) this is a utility, like any other command - its language is an implementation detail that should not be exposed to the user of the utility (you don't care if your `ls` is written in c or go, right? Could be done with a symlink, but that creates two files instead of one - over-complicating. – Yuri Astrakhan Mar 10 '20 at 05:17
  • It it is a `.py` file, you can import it. If it is not, you cannot. – DYZ Mar 10 '20 at 05:18
  • 1
    @DYZ this is NOT a dup - I am not executing the code, I need to `load` it. I think you are incorrect in marking it as a dup (and yes, I saw that post before posting this one) – Yuri Astrakhan Mar 10 '20 at 05:22
  • If you mean you want to _read_ the file, then open it and read it. It is a text file, after all. There is no such thing as _loading_ a file. – DYZ Mar 10 '20 at 05:25
  • 1
    no, of course not :) By loading I mean loading it as a resource/code, i.e. possibly using `SourceFileLoader` -- allowing me to use reflection-like functionality (in .NET parlance) - thus making it possible to call the `foo()` method from the tests. – Yuri Astrakhan Mar 10 '20 at 05:27
  • That's importing, and the dup has an answer for it. – DYZ Mar 10 '20 at 05:30
  • @filbranden thx, but the `importlib.util.spec_from_file_location` keeps returning `None` for my usecase because the file has no `.py` extension. @DYZ could you specify which answer you mean? I have looked at all of them, and runpy method is the closest, while still being a "run" rather than "import" – Yuri Astrakhan Mar 10 '20 at 05:41
  • @filbranden please vote to reopen (gray link right under the post). Thx! – Yuri Astrakhan Mar 10 '20 at 05:42
  • 1
    Yes I'm having the same trouble with `importlib.util`... This works for me: `foo_bar = imp.load_module('foo_bar', f, 'foo-bar', ('', 'r', imp.PY_SOURCE))`, passing it a file object for the 'foo-bar' file as `f`, but the `imp` module is deprecated and suggests `importlib.util` instead... – filbranden Mar 10 '20 at 05:45
  • @filbranden this is awesome! I was able to figure it out thanks to your answer -- `from importlib._bootstrap_external import SourceFileLoader` and `SourceFileLoader('foo-bar', path='.../foo-bar').load_module().foo(42)` !!! I just hope an expert could comment on a better way to access `SourceFileLoader` rather than the import i used. – Yuri Astrakhan Mar 10 '20 at 05:56
  • @filbranden could you post it as an answer and I will accept? thx for all your help! – Yuri Astrakhan Mar 10 '20 at 05:59
  • @Yurik Posted an answer! Thanks for the great question! I'm glad we managed to keep it open as well! – filbranden Mar 10 '20 at 06:07

2 Answers2

1

You can use the functions in importlib to load this module directly from the script file, without a .py extension.

To make this work, you need to use a loader explicitly, in this case SourceFileLoader will work.

from importlib.machinery import SourceFileLoader

foo_bar = SourceFileLoader('foo_bar', './foo-bar').load_module()

At this point, you can use the functions from inside the module:

result = foo_bar.foo(1)
assert result == 43
filbranden
  • 8,522
  • 2
  • 16
  • 32
  • 1
    thank you for helping figure it out! One thing you may want to edit though -- it seems `foo_bar = SourceFileLoader('foo-bar', './foo-bar').load_module()` works just as well, without all the spec stuff, and it lets me call `foo_bar.foo(5)` directly. – Yuri Astrakhan Mar 10 '20 at 06:13
  • @Yurik That's so much simpler! Thank you! – filbranden Mar 10 '20 at 06:22
-1

I think, what you can do is temporarily create copy of the file with extension. py and after importing delete it

  • this would not be a good solution for a testing framework. See above comments - hopefully @ filbranden will post an answer, or I will self-answer in a day. – Yuri Astrakhan Mar 10 '20 at 06:03