|
| 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 | +{: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's been on a bit of a Divine Comedy kick lately 🥴<br><br>anyways this one's on symlinking my way out of <a href="https://twitter.com/hashtag/Python?src=hash&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>— 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: |
0 commit comments