Skip to content

Commit 4cbed82

Browse files
committed
Draft blog on symlinks for python
1 parent 3839b0f commit 4cbed82

File tree

2 files changed

+128
-0
lines changed

2 files changed

+128
-0
lines changed
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
---
2+
layout: post
3+
title: "Be Gone ImportError!! Go Away!!!!"
4+
date: 2021-05-14
5+
no_toc: true
6+
---
7+
8+
I consider myself a moderately fluent Pythonista --- I like to think I can decorate, iterate, & comprehend lists with the best of 'em.
9+
However, organizing Python source --- especially for bigger projects --- has always been a major blind spot for me.
10+
Due to Fear!!, Terror!!, Dread!!, and Severe Tilting!!! over cryptic, intransigent python import errors and [big long stackoverflow pages bereft of copy/paste miracle magic](https://stackoverflow.com/q/14132789) I've always just fallen back to keeping everything in a single monster scripts --- worse yet, duplicating everything across a wicked bramble of sibling script files.
11+
12+
![humorous, relatable memes](/resources/be-gone-lilmayo.png){:style="width: 100%;"}
13+
14+
Organizing and writing with a library mindset is the better way to go:
15+
* source can be stored as small, digestible pieces with descriptive names,
16+
* source can be reused better so there's less duplicated effort and [shotgun surgery](https://en.wikipedia.org/wiki/Shotgun_surgery), and
17+
* source can be pampered with Good Practices like unit testing and formal documentation.
18+
19+
So, this year I've had a bit of a sit-down with myself to try to build some skills in this department with Python.
20+
After rounding my usual miserable circuit of fruitless fiddling and googling, I clocked a new (to me) idea that finally helped me switch over to a library organization model for Python in my projects.
21+
22+
Before we get to my One Weird Trick, let me bore you with a brief description of my long-held project organization dream and
23+
24+
## My Dream K̶i̶t̶c̶h̶e̶n̶ Project
25+
26+
Let's say that I'm working on a very swell codebase called `my_yeet_project`.
27+
I'd like to be able to organize all my reusable python components into a single folder --- maybe called `yeetpylib/`.
28+
I'm going to want to use these components from some Jupyter notebooks.
29+
Lately, I've taken to shoving these all into a folder called `binder/` and hosting them via <https://mybinder.org>.
30+
I usually also want to use these components from a variety of scripts that will be invoked directly from the command-line.
31+
I'd like to organize them in a few folders named based on what they're actually used to do --- typically stuff along the lines of `postprocessing/` and the like.
32+
I'll probably have a few non-python odds and ends (bash :roll_eyes:) living inside `postprocessing/` too.
33+
34+
Here's what a file tree for that project might look like.
35+
36+
```
37+
my_yeet_project
38+
├── binder
39+
│   ├── cheuggy_graphs_n_stats.ipynb
40+
│   └── sheeshish_graphs_n_stats.ipynb
41+
├── postprocessing
42+
│ ├── gaggy_bash_shenanigans.sh
43+
│   └── sheesh_n_cheugify_data.py
44+
└── yeetpylib
45+
├── calculate_cheug.py
46+
   ├── calculate_sheesh.py
47+
└── __init__.py
48+
```
49+
50+
## Why We Can't Have Nice Things
51+
52+
Wouldn't it be nice if we could just access `yeetpylib` by relative import from our scripts and notebooks, something like
53+
54+
`binder/cheuggy_graphs_n_stats.ipyhb` or `postprocessing/sheesh_n_cheugify_data.py`:
55+
```python
56+
import ..yeetpylib.calculate_cheug.cheugrify
57+
```
58+
59+
Wouldn't it, indeed.
60+
:thinking:
61+
62+
Unfortunately, we'll run into a `ImportError: attempted relative import with no known parent package`.
63+
64+
We could just install `yeetpylib` locally with `pip`, but in my *lived experience* this arrangement quickly unravels into an unholy nightmare to try to keep the installed version up-to-date, especially in situations where the library's actively being developed side-by-side with an end-script or when you want scripting to be guaranteed-consistent with your source at a particular revision.
65+
66+
Taking this one step further, we could also bundle `yeetpylib` as a PyPi package.
67+
I've had moderate payoff spamming PyPi with little nuggets I want to re-use across projects (like [keyname](https://pypi.org/project/keyname/), [iterpop](https://pypi.org/project/iterpop/), and [iterdub](https://pypi.org/project/iterdub/)).
68+
However, this only works well for well-defined components are one-and-done-able so you can just pin and forget them.
69+
The local install frustrations mentioned above would pair dreadfully with the aperitif of constantly (forgetting to) deploy your latest changes.
70+
71+
## My One Weird Trick (TM): Symlinking
72+
73+
[Soft symlinks](https://en.wikipedia.org/wiki/Symbolic_link) are a special kind of file that redirects your operating system to go grab a file (or folder) form somewhere else.
74+
For example, if I had this file tree where `my_symlink` was set up to redirect to `../my_file`
75+
76+
```
77+
.
78+
├── my_folder
79+
│   └── my_symlink
80+
└── my_file
81+
```
82+
83+
and I were to run `cat my_folder/my_symlink` the contents of `my_file` would be printed out.
84+
Because they're a redirect rather than a copy, the contents of `my_symlink` will always be up to date with `my_file` (even if we change it later) without us having to do anything!
85+
86+
As a bonus, because they're basically just a file that stores a path to redirect to they [work good with Git](https://mokacoding.com/blog/symliks-in-git/).
87+
(So long as your redirect path is relative and doesn't stretch outside the scope of your repository.)
88+
89+
Soft symlinks are *very* useful as a get-out-of-jail-free card for Python import errors.
90+
If we symlink `yeetpylib` into all the folders with scripts or notebooks where we want to use it, we can freely
91+
```python
92+
import yeetpylib.calculate_cheug.cheugrify
93+
```
94+
to our heart's content!
95+
96+
Here's what a file tree for `my_yeet_project` might look like with symlinks in place.
97+
98+
```
99+
my_yeet_project
100+
├── binder
101+
│   ├── cheuggy_graphs_n_stats.ipynb
102+
│   ├── sheeshish_graphs_n_stats.ipynb
103+
│   └── yeetpylib -> ../yeetpylib
104+
├── postprocessing
105+
│ ├── gaggy_bash_shenanigans.sh
106+
│   ├── sheesh_n_cheugify_data.py
107+
│   └── yeetpylib -> ../yeetpylib
108+
└── yeetpylib
109+
├── calculate_cheug.py
110+
├── calculate_sheesh.py
111+
└── __init__.py
112+
```
113+
114+
To set up these soft symlinks, you can jump into the directories you want to read `yeetpylib` from and run
115+
```bash
116+
ln -s ../yeetpylib yeetpylib
117+
```
118+
119+
## Let's Chat
120+
121+
I would love to hear your thoughts on Python project organization!!
122+
I'm really looking to glow-up my wisdom on wholesome, anti-cringe practices in this space.
123+
124+
I started a twitter thread (right below) so we can chat :phone: :phone: :phone:
125+
126+
<blockquote class="twitter-tweet"><p lang="en" dir="ltr">wow guess my blog&#39;s been on a bit of a Divine Comedy kick lately 🥴<br><br>anyways this one&#39;s on symlinking my way out of <a href="https://twitter.com/hashtag/Python?src=hash&amp;ref_src=twsrc%5Etfw">#Python</a> ImportError hell 🔥🔥🐍🔥<br><br>aight catch u later gotta get back 2 simping for virgil<a href="https://t.co/QnSAo7IsVZ">https://t.co/QnSAo7IsVZ</a> <a href="https://t.co/03JTezsHcy">https://t.co/03JTezsHcy</a> <a href="https://t.co/dUqjrIhihI">pic.twitter.com/dUqjrIhihI</a></p>&mdash; mmore500 (@mmore500) <a href="https://twitter.com/mmore500/status/1393392310554177536?ref_src=twsrc%5Etfw">May 15, 2021</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
127+
128+
Pop on there and drop me a line :fishing_pole_and_fish: or make a comment :raising_hand_woman:

resources/be-gone-lilmayo.png

426 KB
Loading

0 commit comments

Comments
 (0)