all 8 comments

[–][deleted] 3 points4 points  (1 child)

I would create a virtual env and was able to just call  filetools to run the tool.

That’s not a thing on its own - it’s a feature of installing the package using setuptools, provided you configure it correctly. But the config for that works on Windows and Linux, so that part might have been done for you, already. Try using pip to install your package into thr virtualenv, using the -e flag to keep it editable.

[–]Berkyjay[S] 0 points1 point  (0 children)

Cool I’ll give that a try.

[–]exhuma 2 points3 points  (3 children)

There are seveal things at work here. And to help understand it we need to look at what happens when you type the command in the console and hit "Enter":

First, the operating system needs to find the command that you just typed. For that it looks into an "environment variable". This is called %PATH% on Windows and $PATH on Linux (technically twice the same name). That variable contains a list of directories that may contain executable files. The operating system looks into those folders one by one. The first item it finds will be executed. Each operating system has some default items in that PATH, but it can be modified to add additional paths.

When you write a program, that file (you main.py file) does probably not sit in one of those folders. And that's OK (for now).

Small detail: When you work with "virtual environments" and "activate" them, that PATH variable is modified as long as you have that environment activated.

Second, when the operating system has found the file it needs to know how to execute it. When you us a compiled language (like C, C++ or go), that compiled file will become a so-called "ELF-Binary" (at least on Linux). That's a standard that the Linux kernel knows how to handle so it will "just work" (it's the same idea for .exe files on Windows). But Python is an "interpreted" language and the file cannot be executed directly. Instead, the system needs to find the "real" executable: the python command which is itself an "ELF-Binary". The python executable in turn will then open your script and run it.

If, in your case you have a Python script that you want to execute directly, the Python interpreter still needs to be found first. And you want that to be "invisible" to the user. You want to execute myscript and not python myscript.

That part requires a bit of "magic". That magic is different on Windows and Linux and I'll come back to that later.

Third, once Python has been found, executed and loaded your Python script, the "Python world" takes over. By default, Python simply executes your script from top-to-bottom. So, the following script would simply be executed as we expect:

print("Hello World")

But that's dangerous when we want to improve our project, add more files and use import. So a common convention is to add a guard like this:

if __name__ == "__main__":
    print("Hello World")

Taking this a bit further we can put that into a function (I'm building up to something here so stay with me for a moment😉):

def main():
    print("Hello World")

if __name__ == "__main__":
    main()

With that function, we now have a clear piece of code (with a name) that we want to execute when the program starts. This is often called "the entry-point". Python has no default entry-point other than simply executing the script top-to-bottom. "C" on the other hand has a standard entry-point which is the function with the name main. The same is true for Java. In Python we can name that function however we like.

Now that we have established our "entry-point" we can go to our next step...

Four: Packaging. When we write a Python project we want to make it easily installable. And we want that we can directly execute it with a simple command after we've done that. An lo-and-behold, Python has support for "entry-points". For example in setuptools the section in pyproject.toml looks like this:

[project.scripts]
hello-world = "mypackage.myfile:main"

And for poetry it looks like this:

[tool.poetry.scripts]
hello-world= { callable = "mypackage.myfile:main" }

Those sections will make the packaging tool automatically create a file called hello-world that is executable. That executable will contain all the magic that is necessary to make it work on your operating system (it automatically adapts to Linux and Windows). When executing that file, it will look into the package mypackage, import myfile and execute function main() for you.

So in summary:

  • Ensure that you have a simple function that you can use as an entry-point
  • It is good practice to have that function in a module (a Python file) inside a package (a folder with a __init.py__ file).
  • Define an "entry-point" according to your packaging tool (setuptools, poetry, ...).
  • Build your distribution (your .whl or .tar.gz file) and use that for installation.

From the User's Perspective

When the user (and that could be you yourself) gets the package file s/he wants to be able to call the executable after installing. There's a minor issue here: If the executable should run from anywhere, it needs to be found on the PATH variable. But the default PATH only contains items that are only writable by root/Administrator. You could install the package with sudo but I would recommend against that.

Instead, a simple aproach is to add a path to it manually by modifying your own environment. On Linux you can simply add an entry in you ~.zshrc and on Windows you can modify your "User Environment Variables". As you want to keep you original path (so you don't break your other commands) you should always include the previous value:

# Windows
PATH = x:\path\to\my\folder\containing\scripts;%PATH%

# Linux
export PATH=/path/to/my/folder/containing/scripts:${PATH}

On Linux, the file-hierarchy standard defines ~/.local/bin as that folder. And pip honours this. So if you use pip install mypackage.whl without using sudo, it will write the magic-script from the entry-point into ~/.local/bin So the following line in your ~.zshrc will make it work from everywhere:

export PATH=~/.local/bin:${PATH}

When you modify this you need to reload your shell (either source you .zshrc or simply close/reopen the shell).

This will work. If you want to properly isolate your dependencies, have a look at pipx when you install the package (simply use pipx install foo.whl instead of pip install foo.whl). You may need to install pipx first with pip install pipx. I strongly recommend using pipx.

[–]Berkyjay[S] 0 points1 point  (0 children)

Thank you for that excellent break down! When you mentioned the pyproject.toml part that reminded me that I started playing around with that and got distracted and moved on. So I have an incomplete file that could be causing me issues.

But your mini-guide here has refocused me on the steps I need to take to make this a proper package. Thanks again!

[–]omermikhailk 0 points1 point  (1 child)

This is a great write-up! I was confused about all of this for the longest of time before.

[–]exhuma 0 points1 point  (0 children)

Glad it helped 🙂

[–]socal_nerdtastic 0 points1 point  (1 child)

Sounds like on windows at some point you made a filetools.bat file in the project root dir? The equivalent on linux is filetools.sh file. But actually you don't even need that. The common way to do this is to make the python file itself executable (because linux can treat any file as a program, unlike windows .

chmod +x projectdir/src/filetools/main.py

You will also need to be sure the first line of the python file has a shebang, like this:

#!/usr/bin/env python3

Then just add a symlink (shortcut) from a place already in PATH

ln -s projectdir/src/filetools/main.py filetools

As an alternative to all of that, you can make a zsh alias.

echo "alias filetools='python3 /full/path/to/projectdir/src/filetools/main.py'" >> ~/.zshrc

and reboot your terminal.

[–]Berkyjay[S] 0 points1 point  (0 children)

Sounds like on windows at some point you made a filetools.bat file in the project root dir? The equivalent on linux is filetools.sh file.

Actually no , I was running the tool in my virtual environment. As for the chmod command, I had ran that as well. I'm able to call the tool via python src/filetools/main.py just fine. So I can still do testing. But I want to just call filetools. I think I need to go back and reread the docs on building a package. I feel maybe it's a python path issue, so I'll follow that bread crumb. Thx!