This script is used to enable importing local Python modules from your project directory without having to install the project in development mode or modify PYTHONPATH
environment variable manually every time. The .pth
file, is a special file that Python looks for when determining import paths.
Note that you need to be in your local
module
dir, then run these commands.
CURRENT_FOLDER=$(pwd) && echo "$CURRENT_FOLDER"
Then we find the .env
path:
$(poetry env info -p)
will return the virtual environment path (created by Poetry)- Use globbing (
python*
) withls -d
to find the Python version directory - To finally, constructs a path to a file called
project_dir.pth
SITE_PACKAGES_FOLDER="$(ls -d $(poetry env info -p)/lib/python*/site-packages/)project_dir.pth" && echo "$SITE_PACKAGES_FOLDER"
Or, if Poetry is not used, you must first cd
into the .env
directory, then run:
ENV_PATH="$(pwd -L)"
SITE_PACKAGES_FOLDER="$(ls -d $ENV_PATH/lib/python*/site-packages/)project_dir.pth" && echo "$SITE_PACKAGES_FOLDER"
Finally, we append
echo "$CURRENT_FOLDER" >> "$SITE_PACKAGES_FOLDER"
Original Source: StackOverFlow.com
This method allows you to directly reference your project directory in your scripts without creating a .pth
file.
Insert this at the beginning of your Python scripts:
import sys
import os
from pathlib import Path
# Get the absolute path of the module directory. Must change to match your use case.
module_dir = Path().resolve() / "module_dir"
# Add the project root to Python's path
sys.path.append(module_dir)
# Now you can import your local modules
import your_module
This script will allow you to easily add your project's module directories to Python's import path by utilizing a .pth
file within your virtual environment.
#!/usr/bin/env python3
import argparse
import sys
from pathlib import Path
# --- Constants ---
VENV_DIR_NAME = ".venv"
GIT_DIR_NAME = ".git"
PTH_FILE_NAME = "project_dir.pth"
MAX_UPWARD_SEARCH_LEVELS = 10
def find_venv_path(start_dir: Path) -> Path | None:
"""
Finds the .venv directory by searching upwards from start_dir.
The search adheres to the following rules specified by the user:
1. It searches up a maximum of MAX_UPWARD_SEARCH_LEVELS (10) levels. (Rule 2B)
2. If a .git directory is found at a certain level, but a .venv directory
is NOT found at that same level, the search terminates with a failure. (Rule 2A)
3. If the root directory is reached before .venv is found, the search
terminates with a failure. (Rule 2C)
Args:
start_dir: The directory to begin the upward search from.
Typically, this is the module directory or current working directory.
Returns:
The Path object to the .venv directory if found, otherwise None.
"""
current_path = start_dir.resolve()
# Iterate for a maximum of MAX_UPWARD_SEARCH_LEVELS
for _ in range(MAX_UPWARD_SEARCH_LEVELS):
# Check for .venv at the current level
potential_venv_path = current_path / VENV_DIR_NAME
if potential_venv_path.is_dir():
print(f"Found virtual environment at: {potential_venv_path}")
return potential_venv_path # .venv found
# Check for .git at the current level (Rule 2A)
potential_git_path = current_path / GIT_DIR_NAME
if potential_git_path.is_dir():
# If .git is found, .venv should also have been found at this level.
# Since it wasn't (the first 'if' condition failed), this is an error.
print(f"Error: Found '{GIT_DIR_NAME}' at '{current_path}', "
f"but '{VENV_DIR_NAME}' was not found in the same location (Rule 2A).", file=sys.stderr)
return None # Failure as per Rule 2A
# Check if the root directory has been reached (Rule 2C)
if current_path.parent == current_path:
print(f"Error: Reached root directory ('{current_path}') "
"without finding '.venv'. Search stopped (Rule 2C).", file=sys.stderr)
return None # Failure as per Rule 2C
# Move to the parent directory for the next iteration
current_path = current_path.parent
# If the loop completes, .venv was not found within the allowed levels (Rule 2B)
print(f"Error: '{VENV_DIR_NAME}' not found within {MAX_UPWARD_SEARCH_LEVELS} "
f"parent directories of '{start_dir}' (Rule 2B).", file=sys.stderr)
return None
def find_site_packages(venv_path: Path) -> Path | None:
"""
Finds the site-packages directory within a given virtual environment.
It looks for a path like 'venv_path/lib/pythonX.Y/site-packages'.
This is robust for typical virtual environments on Unix-like systems.
Args:
venv_path: The Path object to the .venv directory.
Returns:
The Path object to the site-packages directory if found, otherwise None.
"""
lib_dir = venv_path / "lib"
if not lib_dir.is_dir():
print(f"Error: 'lib' directory not found in virtual environment: '{venv_path}'", file=sys.stderr)
return None
# Glob for pythonX.Y directory, as version can vary
python_version_dirs = list(lib_dir.glob("python*"))
if not python_version_dirs:
print(f"Error: No 'pythonX.Y' directory found in '{lib_dir}'", file=sys.stderr)
return None
for py_dir in python_version_dirs:
site_packages_dir = py_dir / "site-packages"
if site_packages_dir.is_dir():
print(f"Found site-packages directory at: {site_packages_dir}")
return site_packages_dir
print(f"Error: 'site-packages' directory not found within any 'pythonX.Y' "
f"directory in '{lib_dir}'", file=sys.stderr)
return None
def get_paths_to_add(module_dir: Path) -> list[Path]:
"""
Collects the absolute path of the given module directory and all its subdirectories.
Args:
module_dir: The Path object to the main module directory.
Returns:
A list of absolute Path objects, starting with the module_dir itself,
followed by its subdirectories.
"""
# Ensure the module_dir path is absolute and resolved
abs_module_dir = module_dir.resolve()
paths = [abs_module_dir]
# Recursively find all subdirectories within the module_dir
for item in abs_module_dir.rglob('*'):
if item.is_dir():
paths.append(item.resolve()) # Add its absolute, resolved path
return paths
def main():
"""
Main function to parse arguments and orchestrate the path addition.
"""
parser = argparse.ArgumentParser(
description=(
"This script enables importing local Python modules from your project\n"
"directory without having to install the project in development mode or\n"
"modify PYTHONPATH manually. It appends the specified module directory\n"
"and its subdirectories to a '.pth' file in your virtual environment's\n"
"site-packages directory."
),
epilog=(
"Example uses:\n"
" python %(prog)s ./mymodules # Process './mymodules' directory\n"
" python %(prog)s # Process current directory (with warning)\n"
" python %(prog)s /path/to/modules -y # Process and skip confirmation\n\n"
"Search for .venv directory (named '.venv'):\n"
" - The script searches upwards from the effective module directory.\n"
" - Failure if '.git' is found at a level but '.venv' is not at that same level.\n"
" - Failure if search goes up 10 levels without finding '.venv'.\n"
" - Failure if root directory is reached before '.venv' is found."
),
formatter_class=argparse.RawTextHelpFormatter # Preserves formatting in description and epilog
)
parser.add_argument(
"module_dir",
nargs="?", # Makes the argument optional
default=None, # Default is None, will be handled to mean current directory
help=(
"Path to the directory containing your Python modules. "
"If not provided, defaults to the current working directory (a warning will be issued)."
)
)
parser.add_argument(
"-y", "--yes",
action="store_true",
help="Skip the confirmation prompt before making changes."
)
args = parser.parse_args()
# Determine the module path to process
module_target_dir: Path
if args.module_dir is None:
module_target_dir = Path.cwd()
# Rule 1: "default at the current directory with a warning"
print(f"Warning: No module directory specified. "
f"Defaulting to current directory: '{module_target_dir}'", file=sys.stderr)
else:
module_target_dir = Path(args.module_dir)
if not module_target_dir.is_dir():
print(f"Error: Module directory '{module_target_dir.resolve()}' "
"does not exist or is not a directory.", file=sys.stderr)
sys.exit(1)
# Ensure module_target_dir is resolved for consistent use
module_target_dir = module_target_dir.resolve()
# 1. Find the .venv directory (Rule 2)
# The search for .venv starts from the module_target_dir and goes upwards.
venv_directory = find_venv_path(module_target_dir)
if not venv_directory:
# Error message already printed by find_venv_path
sys.exit(1)
# 2. Find the site-packages directory within the .venv
site_packages_directory = find_site_packages(venv_directory)
if not site_packages_directory:
# Error message already printed by find_site_packages
sys.exit(1)
# Construct the full path to the .pth file
pth_file_path = site_packages_directory / PTH_FILE_NAME
# 3. Get all paths to be added (module_dir and its subfolders)
paths_to_write = get_paths_to_add(module_target_dir)
# 4. Confirm changes with the user (unless -y is specified)
print(f"\nThe following paths will be appended to '{pth_file_path}':")
for p_to_add in paths_to_write:
print(f" - {p_to_add}")
print()
if not args.yes:
try:
confirmation = input("Proceed with appending these paths? (y/N): ")
except KeyboardInterrupt:
print("\nOperation cancelled by user (Ctrl+C).")
sys.exit(1)
if confirmation.lower() != 'y':
print("Operation cancelled by user.")
sys.exit(0)
# 5. Append the paths to the .pth file
try:
# Open in append mode ('a'), create if it doesn't exist
with open(pth_file_path, "a", encoding="utf-8") as pth_file:
for p_to_write in paths_to_write:
pth_file.write(f"{p_to_write}\n")
print(f"Successfully updated '{pth_file_path}'.")
print("You might need to restart your Python interpreter or IDE for changes to take effect.")
except IOError as e:
print(f"Error: Could not write to '{pth_file_path}'. Details: {e}", file=sys.stderr)
sys.exit(1)
except Exception as e:
print(f"An unexpected error occurred: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
How to use it:
-
Save the code above into a file, for example,
add_local_modules.py
. -
Make it executable:
chmod +x add_local_modules.py
. -
Run it from your terminal:
-
Specify module directory: If your modules are in a directory like
./my_project_modules
, run:python add_local_modules.py ./my_project_modules
or if it's on your PATH:
./add_local_modules.py ./my_project_modules
-
Default to current directory: If your current directory is the one you want to add (along with its subdirectories):
python add_local_modules.py
(This will show a warning as per your request).
-
Skip confirmation: To automatically proceed without asking for confirmation:
python add_local_modules.py ./my_project_modules -y
-
Get help:
python add_local_modules.py --help
-
This script should fulfill all the requirements you outlined. It's commented to explain each part, uses pathlib
for path operations, and argparse
for a clean command-line interface. Remember that after running this script, you might need to restart any active Python interpreters or your IDE for the changes to the import paths to be recognized.