0

Building a GUI for users to select Python scripts they want to run. Each script has its own docstring explaining inputs and outputs for the script. I want to display that information in the UI once they've highlighted the script, but not selected to run it, and I can't seem to get access to the docstrings from the base program.

ex.

test.py

"""this is a docstring"""
print('hello world')

program.py

index is test.py for this example, but is normally not known because it's whatever the user has selected in the GUI.

# index is test.py
def on_selected(self, index):
    script_path = self.tree_view_model.filePath(index)
    fparse = ast.parse(''.join(open(script_path)))
    self.textBrowser_description.setPlainText(ast.get_docstring(fparse))
martineau
  • 119,623
  • 25
  • 170
  • 301
e4862
  • 19
  • 6
  • 1
    The code you have already here, using [`ast.get_docstring`](https://docs.python.org/3/library/ast.html#ast.get_docstring), seems like the right way to go about this. What is the problem? – wim Mar 01 '21 at 22:48
  • There is already an answer to this [here](https://stackoverflow.com/questions/7791574/how-can-i-print-a-python-files-docstring-when-executing-it) – Comsavvy Mar 01 '21 at 22:50
  • 1
    @Comsavvy the question you referenced is not relevant to this question. – Aaron Mar 01 '21 at 22:52
  • It actually is! @e4862 is looking for a way to get the docstring of `test.py` – Comsavvy Mar 01 '21 at 22:55
  • @Comsavvy the above question refers to how to get the docstring from the `__main__` module not from a separate file. – Aaron Mar 01 '21 at 22:56
  • Did you actually checked the link? Because that is exactly what was answered there. – Comsavvy Mar 01 '21 at 22:57
  • @wim The code doesn't work...kind of why I'm asking for help. – e4862 Mar 01 '21 at 22:57
  • 1
    Unclear why you're doing `ast.parse(''.join(open(script_path)))` instead of an `ast.parse(open(script_path).read())`, – martineau Mar 02 '21 at 01:19
  • @martineau That solved my problem. I was using other stack overflow solutions to try and solve the problem, which is why I was using that join and open. All problems I have you've managed to find the solution to. Please continue being a god at Python. Thank you very much. – e4862 Mar 02 '21 at 02:12

2 Answers2

2

Let's the docstring you want to access belongs to the file, file.py.

You can get the docstring by doing the following:

import file
print(file.__doc__)

If you want to get the docstring before you import it then the you could read the file and extract the docstring. Here is an example:

import re


def get_docstring(file)
    with open(file, "r") as f:
        content = f.read()  # read file
        quote = content[0]  # get type of quote
        pattern = re.compile(rf"^{quote}{quote}{quote}[^{quote}]*{quote}{quote}{quote}")  # create docstring pattern
    return re.findall(pattern, content)[0][3:-3]  # return docstring without quotes


print(get_docstring("file.py"))

Note: For this regex to work the docstring will need to be at the very top.

ArjunSahlot
  • 426
  • 5
  • 13
  • 3
    I think he wants to display it *before* importing, because it's in a menu that the user uses to choose the file to load. – Barmar Mar 01 '21 at 22:41
  • Yeah. I don't know what the file will be. User picks from a list of files that aren't controlled by the program or programmer. The files are placed into a directory and the GUI picks them up for the user to select from. – e4862 Mar 01 '21 at 22:43
  • 2
    @Barmar except, you can't get `file.__doc__` without evaluating the file in some way. the only difference will be semantics like using `importlib.import_module` or even something like `eval` – Aaron Mar 01 '21 at 22:44
  • The next best would be to sift through results from `ast.parse` to find the module level docstring. Otherwise if the files are perfectly consistent maybe you could just skip the shebang line, then read until you see two instances of `'''` or `"""`. Importing is far and away the simplest solution. – Aaron Mar 01 '21 at 22:47
  • How do I import files dynamically? – e4862 Mar 01 '21 at 22:50
  • 2
    See [`importlib`](https://docs.python.org/3/library/importlib.html#module-importlib), – martineau Mar 01 '21 at 22:53
  • @e4862 `importlib.import_module` can import modules by name, but I'm still not clear why your above solution is insufficient. Importing will actually run the code by the way if there's any functional code not protected by `if __name__ == "__main__":` – Aaron Mar 01 '21 at 22:53
  • The program will never know what python files it will be displaying or running until they exist within a designated folder. So I, as the programmer, will also never know what these files will be. So how am I to import the file to reference its docstring without being able to do so dynamically? – e4862 Mar 01 '21 at 22:56
  • 1
    @Aaron: Becase they don't want to import it: "I want to display that information in the UI once they've highlighted the script, but not selected to run it". – martineau Mar 01 '21 at 22:57
  • @e4862: You can get the name of the script file from the GUI, and that's all you'll need to use `importlib` to get the information you want (but you will have import it via `importlib` even if you otherwise discard the other results). – martineau Mar 01 '21 at 23:00
  • @martineau The issue now is that I can't import the module because the file isn't a module. – e4862 Mar 01 '21 at 23:14
  • I would recommend changing this answer to provida function which returns the docstring, instead of printing it out – SpaceKatt Mar 01 '21 at 23:16
  • Good idea @SpaceKatt. – ArjunSahlot Mar 01 '21 at 23:17
  • 2
    @e4862: All Python scripts are modules. – martineau Mar 01 '21 at 23:17
  • @martineau Then it's obviously an issue with the path I'm passing to to importlib. _error_: **Exception has occurred: ModuleNotFoundError**. _code_: pfile = os.path.join(os.environ.get("PROGRAM_ROOT"), "test", "test.py"); importlib.import_module(pfile); test = pfile.__doc__(); print(test);. Sorry, I'm too dumb to figure out comment formatting. – e4862 Mar 01 '21 at 23:23
  • @SaladHead awesome, I upvoted after you made the change – SpaceKatt Mar 01 '21 at 23:25
  • Why is parsing with a regex any better than using `ast.get_docstring`? – wim Mar 02 '21 at 00:56
1

Here's how to get it via importlib. Most of the logic has been put in a function. Note that using importlib does import the script (which causes all its top-level statements to be executed), but the module itself is discarded when the function returns.

If this was the script docstring_test.py in the current directory that I wanted to get the docstring from:

""" this is a multiline
    docstring.
"""
print('hello world')

Here's how to do it:

import importlib.util

def get_docstring(script_name, script_path):
    spec = importlib.util.spec_from_file_location(script_name, script_path)
    foo = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(foo)
    return foo.__doc__

if __name__ == '__main__':

    print(get_docstring('docstring_test', "./docstring_test.py"))

Output:

hello world
 this is a multiline
    docstring.

Update:

Here's how to do it by letting the ast module in the standard library do the parsing which avoids both importing/executing the script as well as trying to parse it yourself with a regex.

This looks more-or-less equivalent to what's in your question, so it's unclear why what you have isn't working for you.

import ast

def get_docstring(script_path):
    with open(script_path, 'r') as file:
        tree = ast.parse(file.read())
        return ast.get_docstring(tree, clean=False)

if __name__ == '__main__':

    print(repr(get_docstring('./docstring_test.py')))

Output:

' this is a multiline\n    docstring.\n'
martineau
  • 119,623
  • 25
  • 170
  • 301
  • I'll have to look into seeing if there is a way to do this without executing the code. Either that or reimagine the GUI to open another window that displays just the docstring once the user selects to run the script. – e4862 Mar 01 '21 at 23:57
  • @SaladHead's [answer](https://stackoverflow.com/a/66430752/355230) does it by attempting to parse the contents of the file, which would likely work most if not all of the time — personally I'd be a little leery of parsing a script with a regex. As far as executing the code goes, most modules don't print out stuff when they're imported (for obvious reasons). – martineau Mar 02 '21 at 00:02
  • The issue is that software dev's aren't in control of writing the scripts, so maintaining consistency is out of our control. Makes it hard to like parsing the file. – e4862 Mar 02 '21 at 00:06
  • The original question was already using `ast.parse`/`ast.get_docstring`... And O.P. never explained what was wrong with that approach. – wim Mar 02 '21 at 00:55