Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/json2d/dotrelay

a utility library for declaring relative imports in Python
https://github.com/json2d/dotrelay

boilerplate-reduction module-import workflow

Last synced: about 1 month ago
JSON representation

a utility library for declaring relative imports in Python

Awesome Lists containing this project

README

        

# πŸ“‘ dotrelay
[![PyPI version](https://badge.fury.io/py/dotrelay.svg)](https://badge.fury.io/py/dotrelay)
[![Build Status](https://travis-ci.com/json2d/dotrelay.svg?branch=main)](https://travis-ci.com/json2d/dotrelay) [![Coverage Status](https://coveralls.io/repos/github/json2d/dotrelay/badge.svg?branch=main)](https://coveralls.io/github/json2d/dotrelay?branch=main)

a utility library for declaring relative imports in Python

## Quick install
```bash
pip install dotrelay
```

## Basic usage
out-of-the-box here's how to use `dotrelay` to import a module from an ancestor directory containing a `.relay` file:

```py
import dotrelay

with dotrelay.Radio(__file__): # πŸ“»
import some_relatively_external_module
```

## Problem

importing _relatively external_ modules is hard (in Python 🐍)

don't believe? just check out this 10+ years of discussion on the internet:
- https://stackoverflow.com/questions/6323860/sibling-package-imports

so forget about importing modules from _another galaxy_:

```
.
β”œβ”€β”€ andromeda
β”‚ └── ufos.py -- πŸ›ΈπŸ›ΈπŸ›Έ
└── milky_way
└── sol
└── earth
β”œβ”€β”€ animals
β”‚ β”œβ”€β”€ __init__.py
β”‚ β”œβ”€β”€ birds.py
β”‚ └── fish.py
β”œβ”€β”€ lands
β”‚ β”œβ”€β”€ __init__.py
β”‚ └── deserts.py
└── waters
β”œβ”€β”€ __init__.py
└── oceans.py
```

in order to import `ufos` into `deserts` you'd need this bit of boilerplate:

```py
# deserts.py
import sys
import os

# get directory path containing `andromeda` (relatively from this module's file path)
root_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__) ) ) ) ) )

sys.path.append(root_path) # extend module import context
from andromeda import ufos # import that thing we need
sys.path.remove(root_path) # cleanup

# finally the real work can begin
def lose_cattle_mysteriously(cattle):
ufos.abduct_cattle(cattle, mode='random')

```

commonly referred to as a "`sys.path` hack", this is what we want to provide a better alternative for - it's fairly low level, fairly ugly, noisy and just plain makes the code smelly πŸ‘ƒπŸ½

## Solution

so let's make this better - we have the technology!

for starters let's create a `.relay` file in the directory containing `andromeda`, the module we want to import into `oceans`

> **_NOTE:_* this `.relay` file must be in one of `oceans` ancestor directories to be discoverable

```
.
β”œβ”€β”€ .relay -- πŸ“‘
β”œβ”€β”€ andromeda
β”‚ └── ufos.py -- πŸ›ΈπŸ›ΈπŸ›Έ
└── milky_way
└── sol
└── earth
β”œβ”€β”€ animals
β”‚ β”œβ”€β”€ __init__.py
β”‚ β”œβ”€β”€ birds.py
β”‚ └── fish.py
β”œβ”€β”€ lands
β”‚ β”œβ”€β”€ __init__.py
β”‚ └── deserts.py
└── waters
β”œβ”€β”€ __init__.py
└── oceans.py
```

now in `oceans` we can use a `dotrelay.Radio` to _discover the `.relay` file above it_ and _establish a kind of temporary bridge_ for us to import `andromeda` and/or other modules in the relay directory

```py
# deserts.py
import dotrelay
with dotrelay.Radio(__file__): # πŸ“»
from andromeda import ufos

def lose_cattle_mysteriously(cattle):
ufos.abduct_cattle(cattle, mode='psuedo-random') # yes it happened
```

now the boilerplate has been reduced to something fairly high level, fairly clean, short and sweet

## Common scenarios
fun example aside, lets see how this fits into real world scenarios
### Testing modules
so here's a typical file structure for most python lib projects where there's the main module and some test modules

```
.
β”œβ”€β”€ pything
β”‚ β”œβ”€β”€ __init__.py
β”‚ └── main.py
└── tests
└── units.py
```

in order to test `pything` it needs to be imported into `units`, and you end up with more of that "`sys.path` hack" bloat:

```py
# units.py
import sys
import os
root_path = os.path.dirname( os.path.dirname( path.abspath(__file__) ) ) # the directory that contains pything
sys.path.append(root_path)

import pything

sys.path.remove(root_path) # cleanup

import unittest

# ...
```

an awkward thing to have to include in every single test module

with `dotrelay` we simply add the `.relay` file:

```
.
β”œβ”€β”€ .relay -- πŸ“‘
β”œβ”€β”€ pything
β”‚ β”œβ”€β”€ __init__.py
β”‚ └── main.py
└── tests
└── units.py
```

and the boilerplate is reduced to:

```py
# tests/units.py
import dotrelay
with dotrelay.Radio(__file__): # πŸ“»
import pything
```

### Organizing modules

building off the previous example, say `units` were to be moved deeper into the project file structure:

```
.
β”œβ”€β”€ .relay -- πŸ“‘
β”œβ”€β”€ pything
β”‚ β”œβ”€β”€ __init__.py
β”‚ └── main.py
└── tests
└── basic
└── units.py
```

with a "`sys.path` hack" the code for getting the `root_path` would need to be updated since again it's relative to the module's own file path

so really then, overtime, as a project matures, this hack becomes something that needs to be manage.

but that can all be avoided with `dotrelay`. no changes need to be made as long as the `.relay` file remains with one of `units` ancestor directories

### Reading static files

sometimes it's also useful just having the path of the relay directory

```
.
β”œβ”€β”€ .relay -- πŸ“‘
β”œβ”€β”€ pything
β”‚ β”œβ”€β”€ __init__.py
β”‚ └── main.py
└── fixtures
β”‚ └── data.json -- πŸ“
└── tests
└── units.py
```

so to read `fixtures/data.json` from `units`:

```py
# tests/units.py
import dotrelay
with dotrelay.Radio(__file__) as rad: # πŸ“»
ROOT_PATH = rad.relay_path

import os, json
DATA_PATH = os.path.join(ROOT_PATH, 'fixtures', 'data.json')
with open(DATA_PATH, 'r') as fp:
DATA = json.load(fp)

import unittest
# ...
```

echoing the point from the previous example, this feature is pretty usefule when you need to move the static files around in a project