Add support for "src directory"-like repo layout

Trytond modules typically use a “flat” repo, where the Python-modules of the package reside in the repo root: <repo>/__init__.py. The down-side of this approach is that all other .py files in the repo are considered part of the package, too, will be put into the bdist and will be installed. E.g. the test package. Another example: I keep a _zest_release_hooks.py in the repo to support my release-process, which should not be installed.

A common approach to circumvent packaging unwanted files would be to move the trytond module package into a src directory and make setuptools pick it up from there, like PyPA describes.

Anyhow, this work-around does not work for trytond modules in develop-mode (pip install -e . or python setup.py develop):

  • trytond modules are namespace modules (trytond.modules.mymodule), which - when using the “flat” repo layout - are not imported properly anyway. This is why trytond.modules implements a work-around:
  • trytond.modules.import_module(), if normal import fails, falls back on searching the module on sys.path and in the module entry-point’s location (ep.dist.location)

Proposal

Extend the already existing work-around in trytond.modules.import_module() to use <repo>/<package-name> if it exists.

This would allow to have a “src directory”-like repo layout: <repo>/mymodule/__init__.py. This restricts the directory name to be the last part of the fully-qualified trytond module name (trytond.modules.mymodule), OTOH, this would allow to have several modules in the same repo.

Patch see below, adding three lines of code.

WDYT?

--- trytond/modules/__init__.py.orig
+++ trytond/modules/__init__.py
@@ -66,9 +66,14 @@
                 if os.path.isdir(os.path.join(path, name)):
                     break
             else:
-                # When testing modules from setuptools location is the
-                # module directory
-                path = os.path.dirname(ep.dist.location)
+                # Check for a "src"-like repository (<repo>/<package>)
+                # installed in "develop" mode
+                path = os.path.join(
+                    ep.dist.location, ep.module_name.split('.')[-1])
+                if not os.path.isdir(path):
+                    # When testing modules from setuptools location is the
+                    # module directory
+                    path = os.path.dirname(ep.dist.location)
         spec = FileFinder(
             path, (SourceFileLoader, SOURCE_SUFFIXES)
             ).find_spec(fullname)

If you are using find_packages it supports including and excluding diferent files/packages.

Have never tried this package layout, but as far as you set the proper entrypoint it should be no issue.

I will prefer to not extend the current package structure to suite any needs of our developers and stick to what Python package features it has. If you explore the possibilities I listed above I’m sure you will be able to find a package structure that works for your layout without altering tryton source code.

Indeed it is possible to have a single python package (on a single repo) that is installed as mutiple modules.
If you want to look at this there is an example on the kalenis backend repository..

I’d be happy to solve this with setuptools means, if possible. Anyhow, I did not manage to get this work with find_package: find_package searches for packages within the current repo, using the directory name as package name. I couldn’t make it into using the top-level directory as the package (which needs to be named)
Does find_package work with the “flat” repo trytond modules typically use? Is there some example?

Proper end-points are not enough (when complying to Tryton module name conventions). trytond.modules.import_module() assumes a specific repo layout.

If you like to check it out yourself: You can find a respective repo at https://gitlab.com/htgoebel/trytond-country_order/, providing three branches: Branch main uses the “flat” repo layout (and installs the release-helper), branch “use-src-subdirectory” uses normal “src directory” layout (and fails to be imported when installed in develop mode) and branch use-country_order-subdirectory uses a layout which the patch could resolve.

Please do not use trytond_ prefix for your package name, it is reserved to official Tryton.
You can use the cookiecutter template to get a proper setup.

We want to have the tests installed so they can be executed and ensure that the installation is working.

I do not think we want to support more special cases (especially any that are not useful for Tryton development).

I do not see you set entry points on any branch.
Please try to understand how the above kalenis example works and adapt to your needs. This way you will be able to structure your package in the layout you prefer.

Fine for me. I’ll choose a different prefix then.

The entry-points are defined in the setup.cfg file, leveraging modern setuptools capabilities. Anyhow, this issue is not about the entry-points, It’s about trytond.modules.import_module() not finding/importing the module (which is defined by an entry-point).

Please also note: This issue occurs if the package is installed in “develop” mode (pip install -e).

Unfortunately this package suffers from the same issue

mkdir /tmp/test-123
cd /tmp/test-123
git clone https://github.com/Kalenis/kalenislims.git
python3 -m venv _venv
. _venv/bin/activate
pip install -e kalenislims  # <--- develop mode!
python -c "import trytond.modules ; trytond.modules.import_module('lims')"
…
  File "/tmp/test/_venv/lib64/python3.7/site-packages/trytond/modules/__init__.py", line 77, in import_module
    if spec.loader:
AttributeError: 'NoneType' object has no attribute 'loader'

The reason for this is line 75 path = os.path.dirname(ep.dist.location) which sets path to /tmp/test-123, while it should be /test/test-123/kalenislims.


Addendum: Proof, the test basically works:

pip uninstall -y kalenis-lims   # remove develop-mode installation
pip install kalenislims/  # <-- no develop-mode
python -c "import trytond.modules ; trytond.modules.import_module('lims')"