# The scope of this project ## Overview The `editables` library provides a set of functions intended to allow build backends to install an editable copy of a project source tree (the *source*) into a Python environment (the *target*). It is *not* a general tool allowing arbitrary mapping of code files to importable mdules. As a consequence, there are a number of constraints on the scenarios supported by the library: * Source files must be laid out in a reasonable project structure. * The target is a standard site-packages directory, managed by tools conforming to Python packaging standards. Editable installations are a development convenience, and are not generally intended for use in a production environment. While this doesn't directly affect the functionality provided by this library, it *does* affect some of the trade-offs made, particularly in terms of security (if an attacker has access to a development environment, the developer has far greater issues than whether or not their editable install mechanism can be exploited). ## Source Layouts The library is designed to support common source layouts, and reasonable variations of those layouts. While the functionality provided is general, and can be used regardless of how the developer lays out their source code, we do not guarantee that it will be possible to support every possible arrangement of code files. When raising a bug or feature request, users should be prepared to explain what source layout they are using, and if it is not compatible with any of the supported layouts described here, they should either be able to reproduce their issue with a supported layout, or be prepared to request that their layout is added to this list as a supported alternative. ### The `src` layout This is by far the most common project layout, and is recommended for most new Python projects. It is described in [the packaging guide] (https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). In this layout, all of the project code is stored in a single directory within the project repository, typically called `src`, and installation consists of simply copying the content of the `src` directory to the target environment. For this layout, the `add_to_path` method of an editable project should be all that is needed in order to create an editable install. ### The "flat" layout This is probably the second most common project layout, and is also described in [the packaging guide] (https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/). It is generally considered inferior to the `src` layout because the project code is mixed in with configuration files, tools and other scripts relating to workflow, and other non-code files. However, it is attractive for single-file projects, and for cases where being able to simply import the project from the source directory, without an installation step, is useful. For this layout, mapping the various top-level files and directories to their expected import names is the recommended approach. However, if the project avoids putting importable `.py` files in the workspace root directory, the `add_to_path` method can be an acceptable (and simpler) alternative. ### Library code stored separately Where a project has a significant amount of library code that is shipped as part of the installed wheel, the developers may choose to keep that library code in a separate directory within the project repository. For example, consider the following project structure: ``` pyproject.toml src myproject __init__.py cli __init__.py ... ... libs utils __init__.py ... ``` When installed, the project should be importable as `import myproject`, with `import myproject.cli`, and `import myproject.libs.utils` importing individual subpackages. One important feature of this layout is that there is no `libs` directory in the `src` tree - we only support grafting an external directory into the module tree structure, *not* merging an external structure with content held within the main source tree. This layout is supported using the `add_to_subpackage` method. For the example given above, the build backend should call `editable_project.add_to_subpackage("myproject.libs", "/libs")` ### Multiple top level import names This library supports projects that install multiple top-level names into the target environment. This can involve a `src` directory with multiple packages in it, or a number of directories and files exposed via `map`, or a combination of both of these approaches. However, it should be noted that we do *not* support installing a top-level name that is already present, owned by another package. The Python import system does technically allow top-level names to appear multiple times. The semantics of this are subtle, though, and the implementation details of this library mean that we don't always match the semantics of an actual install. Rather than try to deal with these differences, we take the simple approach of saying "don't do that". ## Notes on other structures ### The monorepo structure A popular approach for developing Python code in larger projects is to use what is called a "monorepo" - multiple projects maintained within a single VCS repository. Whether you use a monorepo or not is not relevant to this project, though, as the individual project directories are independent. ### Implicit namespace packages Implicit namespace packages are directories that have no `__init__.py` file. The import system treats them specially, considering them to be packages, but unlike normal packages: 1. A namespace package is not considered to be "owned" by any one project. 2. If a namespace package appears multiple times in `sys.path`, the content is merged to produce a single combined import module. 3. The content of a namespace package is re-evaluated dynamically whenever an import occurs, rather than being cached at interpreter startup. These behaviours are built in to the importlib path finder, and cannot be customised by user code. As a result, we only support namespace packages in a very limited number of cases. 1. When `add_to_path` is used, any namespace packages within the added path are handled normally. 2. When `map` is used, *with the self-replace method*, to register an import name that is in a namespace package, for example `ns.mypkg`, the containing namespace will work as normal. Any other uses of namespace packages are not supported. Note in particular that the import hook method of implementing `map` does *not* support namespace paths. Import hooks cannot customise namespace package behaviour, as noted above. ## Additional notes ### Metadata changes If a project's metadata is changed (for example, by editing the `pyproject.toml` file), the metadata of the editable install will *not* change automatically. These changes require a reinstall of the project to take effect. One particular example here is adding or amending entry points to the project. The modified entry points will not be available without a reinstall. ### Binary extensions Native binary extensions are not supported. There are two key issues here: 1. Native code generally requires a compilation step, which needs a detailed understanding of the native build process. This knowledge is part of the build backend, and is not available to a general support library like this one. 2. The module mapping techniques used in this library have only been tested with pure Python modules. No attempt has been made to ensure that they work with binary extensions. In practice, methods that simply add entries to `sys.path` (the `add_to_path` and `add_to_subpackage` methods) will probably work, but other methods generally won't. Build backends that handle native code can, of course, add their own support for editable installs of native binary extensions. ### Filesystem operations Because the editable import mechanisms (particularly the `map` method) break the correspondence between filesystem structure and import package nesting, code which uses filesystem operations to locate files relative to an imported module may not behave as expected. The `add_to_path` and `add_to_subpackage` methods, which simply manipulate `sys.path`, should be fine as long as filesystem operations are constrained to searching *within* the exposed module structure, and not reaching "upwards" in the filesystem layout. ### Package resource access The import hook strategy for `map` does not provide a resource loader as part of its implementation. Nor does it support mapping anything other than Python modules. As a result, `importlib.resources` is not supported. This could be viewed as a bug, but given that the newer self replacing strategy handles the resource API without needing special code, the recommended approach is simply to switch to that strategy. The self-replacing module strategy for `map` does not have this limitation, but because it is only possible to map Python modules, and not data files, complex maps are likely to not work as expected. Care should be taken in this case. As usual, methods which work by manipulating `sys.path` (namely `add_to_path` and `add_to_subpackage`) do not have these limitation, and package resource access should work fine. ### Complex mapping of files It is important to remember that this library is designed for exposing the development sources of a Python package to an interpreter environment. Generally, source layouts should match the structure of the modules being developed fairly closely. As a result, the methods in this library are *not* designed as a general module mapping mechanism. Complex mappings are likely to cause issues, and the recommended solution in such cases is to simplify your development source layout. Most projects should not need much more than a single `add_to_path` call, or maybe one or two `map` calls.