Skip to content

json2d/dotrelay

Repository files navigation

📡 dotrelay

PyPI version Build Status Coverage Status

a utility library for declaring relative imports in Python

Quick install

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:

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:

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:

# 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

# 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:

# 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:

# 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:

# 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