Skip to content

Commit

Permalink
Merge pull request #28 from pfmoore/add_to_subpackage
Browse files Browse the repository at this point in the history
Add a new add_to_subpackage method
  • Loading branch information
pfmoore committed Jun 29, 2023
2 parents 6e6b597 + 72f1567 commit ba28e61
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 1 deletion.
9 changes: 9 additions & 0 deletions docs/source/implementation.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ a `src` subdirectory (with the `src` directory then being what gets added to

The `editables` project implements this approach using the `add_to_path` method.

## Package-specific paths

If a package sets the `__path__` variable to a list of those directories, the
import system will search those directories when looking for subpackages or
submodules. This allows the user to "graft" a directory into an existing package,
simply by setting an appropriate `__path__` value.

The `editables` project implements this approach using the `add_to_subpackage` method.

## Import hooks

Python's import machinery includes an "import hook" mechanism which in theory
Expand Down
15 changes: 15 additions & 0 deletions docs/source/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ 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.

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.

For example, if the directory `src` contains a package `my_pkg`, which you want
to expose to the target interpreter as `some.package.my_pkg`, run the following:

```python
project.add_to_subpackage("some.package", "src")
```

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).

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 Down
14 changes: 13 additions & 1 deletion src/editables/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def __init__(self, project_name: str, project_dir: Union[str, os.PathLike]) -> N
self.project_dir = Path(project_dir)
self.redirections: Dict[str, str] = {}
self.path_entries: List[Path] = []
self.subpackages: Dict[str, Path] = {}

def make_absolute(self, path: Union[str, os.PathLike]) -> Path:
return (self.project_dir / path).resolve()
Expand All @@ -58,11 +59,17 @@ def map(self, name: str, target: Union[str, os.PathLike]) -> None:
else:
raise EditableException(f"{target} is not a valid Python package or module")

def add_to_path(self, dirname: Union[str, os.PathLike]):
def add_to_path(self, dirname: Union[str, os.PathLike]) -> None:
self.path_entries.append(self.make_absolute(dirname))

def add_to_subpackage(self, package: str, dirname: Union[str, os.PathLike]) -> None:
self.subpackages[package] = self.make_absolute(dirname)

def files(self) -> Iterable[Tuple[str, str]]:
yield f"{self.project_name}.pth", self.pth_file()
if self.subpackages:
for package, location in self.subpackages.items():
yield self.package_redirection(package, location)
if self.redirections:
yield f"{self.bootstrap}.py", self.bootstrap_file()

Expand All @@ -80,6 +87,11 @@ def pth_file(self) -> str:
lines.append(str(entry))
return "\n".join(lines)

def package_redirection(self, package: str, location: Path) -> Tuple[str, str]:
init_py = package.replace(".", "/") + "/__init__.py"
content = f"__path__ = [{str(location)!r}]"
return init_py, content

def bootstrap_file(self) -> str:
bootstrap = [
"from editables.redirector import RedirectingFinder as F",
Expand Down
15 changes: 15 additions & 0 deletions tests/test_editable.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ def build_project(target, structure):
for name, content in structure.items():
path = target / name
if isinstance(content, str):
# If the name contains slashes, create any
# required parent directories
path.parent.mkdir(exist_ok=True, parents=True)
path.write_text(content, encoding="utf-8")
else:
build_project(path, content)
Expand Down Expand Up @@ -145,3 +148,15 @@ def test_make_project(project, tmp_path):
import foo

assert Path(foo.__file__) == project / "foo/__init__.py"


def test_subpackage_pth(tmp_path, project):
p = EditableProject(PROJECT_NAME, project)
p.add_to_subpackage("a.b", ".")
structure = {name: content for name, content in p.files()}
site_packages = tmp_path / "site-packages"
build_project(site_packages, structure)
with import_state(extra_site=site_packages):
import a.b.foo

assert Path(a.b.foo.__file__) == project / "foo/__init__.py"

0 comments on commit ba28e61

Please sign in to comment.