Skip to content

Commit

Permalink
Merge pull request #29 from pfmoore/docs_0.4
Browse files Browse the repository at this point in the history
Update documentation for release 0.4
  • Loading branch information
pfmoore committed Jul 6, 2023
2 parents 66deb94 + f1c3c84 commit f047567
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 2 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changes

## Release 0.4

* Add a new `add_to_subpackage` method.
* Add type annotations.
* Internal admin: Switch to nox for automation
* Internal admin: Switch to ruff for linting
* Internal admin: Switch from setuptools to flit_core

## Release 0.3

* Add documentation
Expand Down
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ caption: Contents
---
usage
implementation
use-cases
```

# Indices and tables
Expand Down
22 changes: 20 additions & 2 deletions docs/source/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ Once the project has been created, the backend can specify which files should be
exposed when the editable install is done. There are two mechanisms currently
implemented for this.

### Adding a directory to `sys.path`

To add a particular directory (typically the project's "src" directory) to
`sys.path` at runtime, simply call the `add_to_path` method

Expand All @@ -35,6 +37,8 @@ This will simply write the given directory into the `.pth` file added to the
wheel. See the "Implementation Details" section for more information. Note that
this method requires no runtime support.

### Adding a directory as package content

To expose a directory as a package on `sys.path`, call the `add_to_subpackage`
method, giving the package name to use, and the path to the directory containing
the contents of that package.
Expand All @@ -50,6 +54,12 @@ Note that everything in the source directory will be available under the given
package name, and the source directory should *not* contain an `__init__.py`
file (if it does, that file will simply be ignored).

Also, the target (`some.package` here) must *not* be an existing package that
is already part of the editable wheel. This is because its `__init__.py` file
will be overwritten by the one created by this method.

# Mapping individual files/packages

To expose a single `.py` file as a module, call the `map` method, giving the
name by which the module can be imported, and the path to the implementation
`.py` file. It *is* possible to give the module a name that is not the same as
Expand All @@ -59,13 +69,16 @@ the implementation filename, although this is expected to be extremely uncommon.
project.map("module", "src/module.py")
```

To expose a directory as a package, the `map` method is used in precisely the
same way, but with the directory name:
To expose a directory with an `__init__.py` file as a package, the `map`
method is used in precisely the same way, but with the directory name:

```python
project.map("mypackage", "src/mypackage")
```

The directory *must* be a Python package - i.e., it must contain an `__init__.py`
file, and the target package name must be a top-level name, not a dotted name.

Using the `map` method does require a runtime support module.

## Build the wheel
Expand All @@ -84,6 +97,11 @@ for name, content in my_project.files():
wheel.add_file(name, content)
```

Note that the files to be added must be included unchanged - it is *not*
supported for the caller to modify the returned content. Also, it is the
caller's responsibility to ensure that none of the generated files clash with
files that the caller is adding to the wheel as part of its own processes.

### Runtime dependencies

If the `map` method is used, the resulting wheel will require that the runtime
Expand Down
120 changes: 120 additions & 0 deletions docs/source/use-cases.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# Use Cases

We will cover here the main supported use cases for editable installs,
including the recommended approaches for exposing the files to the
import system.

## Project directory installed "as is"

A key example of this is the recommended "`src` layout" for a project,
where a single directory (typically named `src`) is copied unchanged
into the target site-packages.

For this use case, the `project.add_to_path` method is ideal, making
the project directory available to the import system directly.

There are almost no downsides to this approach, as it is using core
import system mechanisms to manage `sys.path`. Furthermore, the method
is implemented using `.pth` files, which are recognised by static analysis
tools such as type checkers, and so editable installs created using this
method will be visible in such tools.

## Project directory installed under an explicit package name

This is essentially the same as the previous use case, but rather than
installing the project directory directly into site-packages, it is
installed under a partocular package name. So, for example, if the
project has a `src` directory containing a package `foo` and a module
`bar.py`, the requirement is to install the contents of `src` as
`my.namespace.foo` and `my.namespace.bar`.

For this use case, the `project.add_to_subpackage` method is available.
This method creates the `my.namespace` package (by installing an `__init__.py`
file for it into site-packages) and gives that package a `__path__` attribute
pointing to the source directory to be installed under that package name.

Again, this approach uses core import system mechanisms, and so will have
few or no downsides at runtime. However, because this approach relies on
*runtime* manipulation of `sys.path`, it will not be recognised by static
analysis tools.

## Installing part of a source directory

The most common case for this is a "flat" project layout, where the
package and module files to be installed are stored alongside project
files such as `pyproject.toml`. This layout is typically *not* recommended,
particularly for new projects, although older projects may be using this
type of layout for historical reasons.

The core import machinery does not provide a "native" approach supporting
excluding part of a directory like this, so custom import hooks are needed
to implement it. At the time of writing, all such custom hook implementations
have limitations, and should be considered experimental. As a result, build
backends should *always* prefer one of the other implementation methods when
available.

The `project.map` method allows mapping of either a single Python file, or
a Python package directory, to an explicit top-level name in the import system.
It does this by installing a `.pth` file and a Python module. The `.pth` file
simply runs the Python module, and the module installs the requested set of
mappings using an import hook exported by the `editables` module.

Downsides of this approach are:

1. The approach depends on the ability to run executable code from a `.pth`
file. While this is a supported capability of `.pth` files, it is
considered a risk, and there have been proposals to remove it. If that
were to happen, this mechanism would no longer work.
2. It adds a *runtime* dependency on the `editables` module, rather than
just a build-time dependency.
3. The import hook has known limitations when used with implicit namespace
packages - there is [a CPython issue](https://github.com/python/cpython/issues/92054)
discussing some of the problems.

## Unsupported use cases

In addition to the above there are a number of use cases which are explicitly
**not** supported by this library. That is not to say that editable installs
cannot do these things, simply that the build backend will need to provide
its own support.

### Metadata changes

This library does not support dynamically changing installed project metadata
when the project source changes. Typically, a reinstall is needed in those
cases. A significant example of a metadata change is a change to the script
entry points, which affects what command-line executables are installed.

### Binary extensions

Binary extensions require a build step when the source code is changed. This
library does not support any sort of automatic rebuilding, nor does it
support automatic reinstallation of binaries.

The build backend may choose to expose the "working" version of the built
binary, for example by placing a symbolic link to the binary in a directory
that is visible to the import system as a result of `project.add_to_path`,
but that would need to be implemented by the backend.

### Mapping non-Python directories or files

The methods of an editable project are all intended explicitly for exposing
*Python code* to the import system. Other types of resource, such as data
files, are *not* supported, except in the form of package data physically
located in a Python package directory in the source.

### Combining arbitrary code into a package

The library assumes that a typical project layout, at least roughly, matches
the installed layout - and in particular that Python package directories are
"intact" in the source. Build backends can support more complex structures,
but in order to expose them as editable installs, they need to create some
form of "live" reflection of the final layout in a local directory (for
example by using symbolic links) and create the editable install using that
shadow copy of the source.

It is possible that a future version of this library may add support for
more complex mappings of this form, but that would likely require a
significant enhancement to the import hook mechanism being used, and would
be a major, backward incompatible, change. There are currently no plans for
such a feature, though.

0 comments on commit f047567

Please sign in to comment.