a utility library for declaring relative imports in Python
pip install dotrelay
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
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 👃🏽
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 ofoceans
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
fun example aside, lets see how this fits into real world scenarios
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
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
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