# 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", "<project root>/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.
