====== Packaging ======
FIXME :!: The Python community has some PEPs and a lot of debate over what the 'correct' way to package Python programs is. As of November 2023 I'm not totally sure what the way forward is meant to be for PyPI projects, or if the below information is still true about Python's module handling. Consume appropriate amounts of salt where sus. :!:
In Python, individual source files are referred to as **modules**. They represent a single collection of code, and can be ''import''ed. **Packages** are directories containing an ''%%__init__.py%%'' file and other modules. They are comparable to namespaces, and can also be ''import''ed. A package with an empty ''%%__init__.py%%'' file is completely fine; it just won't import any modules when imported.
Here's an example of a //package// containing //modules//:
my_package/
__init__.py
ui.py
attribute.py
model.py
router.py
There are multiple ways to interact with a package, which are covered below. In every case, it's assumed that the Python interpreter is in the directory //above// ''my_package/'', or ''my_package/'' is somewhere in Python's path variable. See [[https://docs.python.org/3/library/sys.html#sys.path|Python's documentation on sys.path]] for information on that.
===== Simple Importing =====
# Import just the package
import my_package
In this case, importing the package will process everything that's in ''%%__init__.py%%'', including any pre-defined ''import''s. It's considered good practice to define which modules get imported by default when importing a package, to ensure only the necessary modules are loaded. Your modules can individually ''import'' any part of the package they need to get the job done. To do this, your ''%%__init__.py%%'' should look somewhat like this:
# Let's say you only need the model and router to get started
from . import (model, router)
The ''.'' here means "the current package". If you have sub-packages, you can address them with a dot preceding its name, like ''.my_subpackage''.
===== Single Module Importing =====
Sometimes, you only need a single module from a package. That's easy:
# Import only the UI
from my_package import ui
In this example, //only the ''ui'' module is imported//. It gets injected into the current environment as ''ui'', and you can access its members as well, using something like ''ui.create_box()''.
===== Single Member Importing =====
Lastly, sometimes you need only //specific parts of a module//. That's easy, too!
# Import only specific things from a given module
from my_package.router import do_route
In the above example, ''do_route'' will become available in the current environment, imported from the ''router'' module inside the ''my_package'' package.
If you find yourself with strange ''NameError''s, ''AttributeError''s, or ''ValueError''s, it may point to a name collision. You can rename your imports with the ''as'' keyword:
# Import with a different name
from my_package.router import do_route as make_route
This is often caused by importing two modules with the same member name. If you import two things with the same member name, the latter name will take precedence:
# Same name, different structures
from os import path
print(type(path))
#
from sys import path
print(type(path))
#
For this reason, it's generally better to leave imports alone and go one level deeper only as needed. So if you wanted to use ''path'' from both ''os'' and ''sys'', just import them on their own, so you're using ''os.path'' and ''sys.path''. It's more verbose, but also more precise and less likely to break.
====== References ======
* [[https://www.learnpython.org/en/Modules_and_Packages]]
* [[https://docs.python.org/3/tutorial/modules.html]]
~~DISCUSSION~~