├── src ├── tests │ ├── __init__.py │ ├── __pycache__ │ │ ├── __init__.cpython-39.pyc │ │ ├── __init__.cpython-310.pyc │ │ ├── __init__.cpython-312.pyc │ │ ├── test_main.cpython-310-pytest-7.3.1.pyc │ │ ├── test_main.cpython-312-pytest-7.4.3.pyc │ │ └── test_main.cpython-39-pytest-7.3.1.pyc │ └── test_main.py ├── pytest.ini └── slushie │ ├── __init__.py │ ├── __pycache__ │ ├── main.cpython-39.pyc │ ├── main.cpython-310.pyc │ ├── main.cpython-312.pyc │ ├── __init__.cpython-310.pyc │ ├── __init__.cpython-312.pyc │ └── __init__.cpython-39.pyc │ └── main.py ├── example ├── folder2 │ ├── file.txt │ └── folder3 │ │ ├── __init__.py │ │ ├── funs.py │ │ └── __pycache__ │ │ └── funs.cpython-312.pyc ├── folder1 │ ├── main.py │ └── __pycache__ │ │ └── main.cpython-312.pyc └── test.py ├── .gitignore ├── media └── slushie.gif ├── run_tests.bat ├── run_tests.sh ├── LICENSE ├── setup.py └── README.md /src/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/folder2/file.txt: -------------------------------------------------------------------------------- 1 | hello world -------------------------------------------------------------------------------- /example/folder2/folder3/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /example/folder1/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | print(sys.path) -------------------------------------------------------------------------------- /src/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | testpaths = 3 | tests 4 | -------------------------------------------------------------------------------- /example/folder2/folder3/funs.py: -------------------------------------------------------------------------------- 1 | def hello(): 2 | print("hello") -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .benchmarks 2 | .pytest_cache 3 | src/slushie.egg-info 4 | build 5 | dist -------------------------------------------------------------------------------- /media/slushie.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/media/slushie.gif -------------------------------------------------------------------------------- /src/slushie/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import sip, gulp, pour, melt, slurp, scoop, freeze 2 | -------------------------------------------------------------------------------- /src/slushie/__pycache__/main.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/slushie/__pycache__/main.cpython-39.pyc -------------------------------------------------------------------------------- /src/slushie/__pycache__/main.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/slushie/__pycache__/main.cpython-310.pyc -------------------------------------------------------------------------------- /src/slushie/__pycache__/main.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/slushie/__pycache__/main.cpython-312.pyc -------------------------------------------------------------------------------- /src/tests/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/tests/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /example/folder1/__pycache__/main.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/example/folder1/__pycache__/main.cpython-312.pyc -------------------------------------------------------------------------------- /example/test.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | 5 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__)))) 6 | 7 | import funs -------------------------------------------------------------------------------- /src/slushie/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/slushie/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /src/slushie/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/slushie/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /src/slushie/__pycache__/__init__.cpython-39.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/slushie/__pycache__/__init__.cpython-39.pyc -------------------------------------------------------------------------------- /src/tests/__pycache__/__init__.cpython-310.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/tests/__pycache__/__init__.cpython-310.pyc -------------------------------------------------------------------------------- /src/tests/__pycache__/__init__.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/tests/__pycache__/__init__.cpython-312.pyc -------------------------------------------------------------------------------- /example/folder2/folder3/__pycache__/funs.cpython-312.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/example/folder2/folder3/__pycache__/funs.cpython-312.pyc -------------------------------------------------------------------------------- /src/tests/__pycache__/test_main.cpython-310-pytest-7.3.1.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/tests/__pycache__/test_main.cpython-310-pytest-7.3.1.pyc -------------------------------------------------------------------------------- /src/tests/__pycache__/test_main.cpython-312-pytest-7.4.3.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/tests/__pycache__/test_main.cpython-312-pytest-7.4.3.pyc -------------------------------------------------------------------------------- /src/tests/__pycache__/test_main.cpython-39-pytest-7.3.1.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/saleguas/slushie/HEAD/src/tests/__pycache__/test_main.cpython-39-pytest-7.3.1.pyc -------------------------------------------------------------------------------- /run_tests.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | REM Activate virtual environment if exists 4 | if exist "venv" ( 5 | .\venv\Scripts\activate 6 | ) 7 | 8 | REM Install the package in editable mode 9 | pip install -e . 10 | 11 | REM Run pytest to execute tests 12 | pytest 13 | 14 | REM Deactivate virtual environment if exists 15 | if exist "venv" ( 16 | deactivate 17 | ) 18 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Activate virtual environment if exists 4 | if [ -d "venv" ]; then 5 | source venv/bin/activate 6 | fi 7 | 8 | # Install the package in editable mode 9 | pip install -e . 10 | 11 | # Run pytest to execute tests 12 | pytest 13 | 14 | # Deactivate virtual environment if exists 15 | if [ -d "venv" ]; then 16 | deactivate 17 | fi 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sal 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup( 4 | name='slushie', 5 | version='0.2.4', 6 | packages=find_packages(where='src'), 7 | package_dir={'': 'src'}, 8 | 9 | # Dependencies 10 | install_requires=[ 11 | # Add your dependencies here (e.g., 'numpy>=1.18.5') 12 | ], 13 | 14 | # Metadata 15 | author='Salvador Aleguas', 16 | author_email='salvadoraleguas@example.com', # Add your email here 17 | description='Slushie: A Python library for relative path manipulation.', 18 | long_description=open('README.md', 'r', encoding='utf-8').read(), 19 | long_description_content_type='text/markdown', 20 | license='MIT', 21 | keywords='path sys-path manipulation python-path module-path', 22 | url='https://github.com/saleguas/slushie', 23 | 24 | # Specifying python requires 25 | python_requires='>=3', 26 | 27 | # Classifier (Optional) 28 | classifiers=[ 29 | 'Development Status :: 3 - Alpha', 30 | 'Intended Audience :: Developers', 31 | 'License :: OSI Approved :: MIT License', 32 | 'Programming Language :: Python :: 3', 33 | 'Programming Language :: Python :: 3.12', 34 | ], 35 | ) 36 | -------------------------------------------------------------------------------- /src/tests/test_main.py: -------------------------------------------------------------------------------- 1 | import os, sys 2 | import pytest 3 | 4 | # This line is incredibly ironic and also the reason why this library exists lmao 5 | sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'slushie'))) 6 | 7 | from slushie import sip, gulp, pour, melt, slurp, scoop, freeze 8 | 9 | def test_sip(): 10 | # Test if sip() returns the correct absolute path 11 | assert os.path.isabs(sip('test')) 12 | 13 | def test_gulp(): 14 | # Test if gulp() correctly modifies sys.path 15 | directory = 'test_directory' 16 | with gulp(directory): 17 | assert any(directory in path for path in sys.path) 18 | 19 | def test_freeze(): 20 | # Test if freeze() correctly modifies sys.path 21 | freeze('test_directory') 22 | assert any('test_directory' in path for path in sys.path) 23 | 24 | def test_pour(): 25 | # Test if pour() returns the correct current and parent directories 26 | with pour('test_directory') as (current_dir, parent_dir): 27 | assert current_dir.endswith('test_directory') 28 | assert os.path.basename(parent_dir) != 'test_directory' 29 | 30 | def test_melt(): 31 | # Test if melt() returns the directory of the calling function/script 32 | def test_function(): 33 | return melt() 34 | assert test_function() == os.path.dirname(__file__) 35 | 36 | def test_slurp(): 37 | # Test if slurp() returns the current working directory 38 | assert slurp() == os.getcwd() 39 | 40 | def test_scoop(tmp_path): 41 | # Test if scoop() correctly opens and writes to a file 42 | file_path = tmp_path / "test.txt" 43 | with scoop(file_path, 'w') as file: 44 | file.write('Hello, Slushie!') 45 | with open(file_path, 'r') as file: 46 | assert file.read() == 'Hello, Slushie!' 47 | with scoop(file_path, 'a') as file: 48 | file.write(' Hello, World!') 49 | with open(file_path, 'r') as file: 50 | assert file.read() == 'Hello, Slushie! Hello, World!' 51 | # test encoding 52 | with scoop(file_path, 'w', encoding='utf-8') as file: 53 | file.write('Hello, Slushie!') 54 | with open(file_path, 'r', encoding='utf-8') as file: 55 | assert file.read() == 'Hello, Slushie!' 56 | -------------------------------------------------------------------------------- /src/slushie/main.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | from contextlib import contextmanager 4 | from typing import Tuple, Iterator, Any, TextIO, Optional 5 | import inspect 6 | 7 | 8 | def sip(*parts: str) -> str: 9 | """ 10 | Get the absolute path relative to the file that is calling this function. 11 | 12 | :param parts: Parts of the path to join. 13 | :return: Absolute path. 14 | """ 15 | # Get the frame of the caller 16 | caller_frame = inspect.stack()[1] 17 | # Retrieve the path of the file from which this function was called 18 | caller_path = caller_frame.filename 19 | # Get the directory of the caller file 20 | caller_dir = os.path.dirname(os.path.abspath(caller_path)) 21 | # Join the caller directory with the specified parts 22 | return os.path.abspath(os.path.join(caller_dir, *parts)) 23 | 24 | @contextmanager 25 | def gulp(directory: str = '.') -> Iterator[None]: 26 | """ 27 | Context manager to temporarily add all subdirectories of a directory to sys.path. 28 | 29 | :param directory: Directory to add subdirectories from. 30 | """ 31 | directory = sip(directory) # Using sip() to get absolute path relative to script 32 | old_path = sys.path.copy() 33 | sys.path.append(directory) 34 | for root, dirs, files in os.walk(directory): 35 | sys.path.append(root) 36 | yield 37 | sys.path = old_path 38 | 39 | def freeze(path: str) -> None: 40 | """ 41 | Append a specific path to sys.path. 42 | 43 | :param path: Path to append to sys.path. 44 | """ 45 | sys.path.append(sip(path)) 46 | 47 | @contextmanager 48 | def pour(directory: str = '.') -> Iterator[Tuple[str, str]]: 49 | """ 50 | Context manager to get the current and parent directory paths. 51 | 52 | :param directory: Directory to get paths for. 53 | :return: Current and parent directory paths. 54 | """ 55 | current_dir, parent_dir = sip(directory), sip(directory, '..') # Using sip() 56 | yield current_dir, parent_dir 57 | 58 | def melt() -> str: 59 | """ 60 | Get the directory of the calling script. 61 | 62 | :return: Directory of the calling script. 63 | """ 64 | caller_frame = inspect.stack()[1] 65 | caller_path = os.path.dirname(os.path.abspath(caller_frame.filename)) 66 | return caller_path 67 | 68 | def slurp() -> str: 69 | """ 70 | Get the directory where the terminal command is executed. 71 | 72 | :return: Directory where the terminal command is executed. 73 | """ 74 | return os.getcwd() 75 | 76 | 77 | def scoop(file: str, mode: str = 'r', buffering: Optional[int] = -1, 78 | encoding: Optional[str] = None, errors: Optional[str] = None, 79 | newline: Optional[str] = None, closefd: bool = True, 80 | opener: Optional[Any] = None) -> TextIO: 81 | """ 82 | Shortcut to open a file with a path relative to the current script's directory. 83 | 84 | :param file: File path relative to the current script's directory. 85 | :param mode: Mode in which the file is opened. 86 | :param buffering: Buffering policy. 87 | :param encoding: Encoding format. 88 | :param errors: Specifies how encoding and decoding errors are to be handled. 89 | :param newline: Controls how universal newlines mode works. 90 | :param closefd: If closefd is False and a file descriptor rather than a filename is given, 91 | the underlying file descriptor will be kept open when the file is closed. 92 | :param opener: A custom opener. 93 | :return: File object. 94 | """ 95 | file_path = sip(file) 96 | return open(file_path, mode, buffering, encoding, errors, newline, closefd, opener) 97 | 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Slushie 🍧 2 | 3 | 🍭 Ever wanted to just get a file from a sibling or parent directory without pulling your hair out? Slushie is the perfect "it just works" solution to relative paths in Python. 4 | 5 | 6 | 7 | ![Slushie Demo](media/slushie.gif) 8 | 9 | ## Table of Contents 10 | 11 | - [❔ Why Slushie?](#-why-slushie) 12 | - [🚀 Installation](#-installation) 13 | - [🌈 Usage](#-usage) 14 | - [🔬 Running Tests](#-running-tests) 15 | - [🤝 Contributing](#-contributing) 16 | - [📜 License](#-license) 17 | 18 | ## ❔ Why Slushie? 19 | 20 | Relative paths and imports in Python are an absolute nightmare due to how `PYTHONPATH` works and finds modules. 21 | 22 | For example, 23 | ``` 24 | project_root/ 25 | │ main.py 26 | │ 27 | ├───package1/ 28 | │ │ module1.py 29 | │ │ file.csv 30 | │ │ 31 | │ └───subpackage1/ 32 | │ │ module2.py 33 | ``` 34 | 35 | If I wanted to import `module1.py` from `main.py`, you'd think it would be something like the following: 36 | 37 | ``` 38 | from package1 import module1 39 | from package1.subpackage1 import module2 40 | ``` 41 | 42 | 43 | 44 | **This (most likely) will not work. Why?** 45 | 46 | Python relies on the dreaded `PYTHONPATH` environment variable to determine where to look for modules to import. 47 | 48 | `PYTHONPATH` is a list of directories that Python checks whenever you attempt an import. If `package1` and `subpackage1` are not included in the `PYTHONPATH`, Python doesn’t know where to look for `module1.py`, and `module2.py`, resulting in an `ImportError`. 49 | 50 | Additionally, attempting to open `file.csv`, using the traditional open command like this: 51 | 52 | ``` 53 | open("package1/file.csv") 54 | ``` 55 | 56 | will most likely not even find the file, and even if it does, there's a high chance it will break if it is ever moved to another machine or ran from a different directory. 57 | 58 | This is because the search for `file.csv` is relative to the current working directory where the Python script is executed, not necessarily where main.py is located. 59 | 60 | **TL;DR: If you use python's default import and open commands, you either have to do some Python witchcraft or risk randomly breaking your code.** 61 | 62 | ## 🚀 Installation 63 | 64 | Install it directly from PyPI: 65 | 66 | ``` 67 | pip install slushie 68 | ``` 69 | 70 | ## 🌈 Usage 71 | 72 | 73 | ### sip(*parts: str) -> str 74 | 75 | **Purpose**: Create absolute paths relative to the current FILE. Ideal for accessing files in parent or sibling directories without a fuss. 76 | 77 | #### Parameters: 78 | - `*parts: str` - Parts of the path to join. 79 | 80 | #### Usage: 81 | Access `hello.txt` located in a sibling directory from `script.py`. 82 | 83 | ``` 84 | /project 85 | /folder1 86 | script.py 87 | /folder2 88 | hello.txt 89 | ``` 90 | 91 | ```python 92 | path = sip('..', 'folder2', 'hello.txt') 93 | print(path) 94 | 95 | # Output: 96 | # /path/to/project/folder2/hello.txt 97 | # In this case, sip('.') refers to /path/to/project/folder1/ 98 | ``` 99 | 100 | The above code is fundamentally equivalent to the following: 101 | 102 | ```python 103 | import os 104 | import sys 105 | 106 | path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'folder2', 'hello.txt') 107 | print(path) 108 | 109 | # Output: 110 | # /path/to/project/folder2/hello.txt 111 | ``` 112 | 113 | This is extremely useful, as if you ever need to open a file, such as a csv for data analysis or a text file for logging, you should almost always be using relative paths as to avoid breaking your code when you move it to a different machine or share it with someone else. Slushie makes this easy. 114 | 115 | ```python 116 | ### gulp(directory: str = '.') -> Iterator[None] 117 | 118 | **Purpose**: Temporarily include directories in the Python path, easing the import of modules/packages. 119 | 120 | #### Parameters: 121 | - `directory: str` - Directory to add directories from. 122 | 123 | #### Usage: 124 | Import a module from a sibling directory. 125 | 126 | ```python 127 | with gulp('../sibling_directory'): 128 | import a_module_from_sibling_directory 129 | ``` 130 | 131 | ### freeze(path: str) -> None 132 | 133 | **Purpose**: Make a specific directory permanently available for imports. 134 | 135 | #### Parameters: 136 | - `path: str` - Path to append to `sys.path`. 137 | 138 | #### Usage: 139 | ```python 140 | freeze('../another_directory') 141 | import a_module_from_another_directory 142 | ``` 143 | 144 | ### pour(directory: str = '.') -> Iterator[Tuple[str, str]] 145 | 146 | **Purpose**: Easily access the current and parent directory paths of the current file the code is being written in. 147 | 148 | #### Parameters: 149 | - `directory: str` - Directory to get paths for. 150 | 151 | #### Usage: 152 | ```python 153 | with pour() as (current_dir, parent_dir): 154 | print(f"Current Directory: {current_dir}") 155 | print(f"Parent Directory: {parent_dir}") 156 | ``` 157 | 158 | ### melt() -> str 159 | 160 | **Purpose**: Find the directory of the calling script, aiding in understanding the execution context. 161 | 162 | #### Usage: 163 | ```python 164 | caller_path = melt() 165 | print(f"Caller Path: {caller_path}") 166 | 167 | # Output: 168 | # Caller Path: /path/to/calling/script.py 169 | # This is the path of the script that called melt(), not the path of melt() itself. 170 | # So if I had script /path/to/calling/script.py that called melt(), and melt() was located at /path/to/melt.py, the output would still be: 171 | # Caller Path: /path/to/calling/script.py 172 | ``` 173 | 174 | 175 | ### slurp() -> str 176 | 177 | **Purpose**: Identify where the terminal command was executed from. 178 | 179 | #### Usage: 180 | ```python 181 | terminal_path = slurp() 182 | print(f"Terminal Path: {terminal_path}") 183 | 184 | # So if the script was located at /path/to/script.py and the terminal command was executed from /path/to, the output would be: 185 | # Terminal Path: /path/to 186 | ``` 187 | 188 | ### scoop(file: str, mode: str = 'r', ...) -> TextIO 189 | 190 | **Purpose**: Simplify opening files by managing paths relative to the current script automatically. 191 | 192 | #### Parameters: 193 | Literally the same as the built-in `open()` function. It's just a wrapper around it that automatically manages paths relative to the current script. 194 | 195 | 196 | #### Usage: 197 | ```python 198 | with scoop('../data.txt', 'r') as file: 199 | data = file.read() 200 | print(data) 201 | ``` 202 | 203 | ## 🔬 Running Tests 204 | 205 | Keeping Slushie frosty with some cool tests: 206 | 207 | - **For Linux:** 208 | ``` 209 | ./run_tests.sh 210 | ``` 211 | 212 | - **For Windows:** 213 | ``` 214 | run_tests.bat 215 | ``` 216 | 217 | ## 🤝 Contributing 218 | 219 | Contribute your own flavors to make Slushie even more delightful! 🌈 220 | 221 | ## 📜 License 222 | 223 | Slushie is lovingly served under the MIT License. Scoop into the [LICENSE](LICENSE) file for the full details. 224 | --------------------------------------------------------------------------------