paths provides project-aware path handling, streamlining how you handle paths and ensuring cross-platform compatibility.
Use it to work with paths relative to your project root, regardless of where your code is executed from.
Skpath Enhanced path object that detects your project root. Cross-platform compatible.
autopath Decorator for automatic path type conversion. Smack it on all of your functions that work with paths, and no more type mismatches will ever happen again.
There are a lot of other random annoying things you might come across when working with paths. Many of them are packed in here.
from suitkaise import paths
from suitkaise .paths import Skpath , autopath , AnyPath , PathDetectionError , NotAFileError , CustomRoot , set_custom_root , get_custom_root , clear_custom_root , get_project_root , get_caller_path , get_current_dir , get_cwd , get_module_path , get_id , get_project_paths , get_project_structure , get_formatted_project_tree , is_valid_filename , streamline_path , streamline_path_quick
Skpath Enhanced path object with automatic project root detection.
All paths use normalized separators (/) for cross-platform consistency.
from suitkaise .paths import Skpath
# create from caller's file path
path = Skpath( )
# create from string
path = Skpath( "myproject/feature/file.txt")
# create from Path object
from pathlib import Path
path = Path("myproject/feature/file.txt")
path = Skpath( path)
# create from encoded ID
path = Skpath( "bXlwcm9qZWN0L2ZlYXR1cmUvZmlsZS50eHQ")
Arguments
path: Path to wrap.
str | Path | Skpath | None = NoneNone, uses the caller's file path: Absolute path with normalized separators (/).
str: Relative path to project root.
str: Reversible base64url encoded ID.
strSkpath( encoded_id)root: Project root as Skpath object.
Skpath root_str: Project root as string with normalized separators.
strroot_path: Project root as pathlib.Path object.
Pathpath = Skpath( "src/main.py")
path.ap # "/Users/me/myproject/src/main.py"
path.rp # "src/main.py"
path.id # "c3JjL21haW4ucHk"
path.root # Skpath('/Users/me/myproject')
name: Final component (filename with extension).
strstem: Final component without suffix.
strsuffix: File extension.
strsuffixes: All file extensions.
list[str]parent: Parent directory as Skpath.
Skpath parents: All parent directories as Skpath objects.
tuple[Skpath , ...]parts: Path components as tuple.
tuple[str, ...]exists: Whether the path exists.
boolis_file: Whether the path is a file.
boolis_dir: Whether the path is a directory.
boolis_symlink: Whether the path is a symbolic link.
boolstat: Stat info for the path.
os.stat_resultlstat: Stat info for the path (don't follow symlinks).
os.stat_resultis_empty: Whether the path is an empty directory.
boolNotADirectoryError if path is not a directoryas_dict: Dictionary representation of the path.
dict[str, Any]ap , rp , root, name, exists: Absolute path with OS-native separators.
strUse the / operator to join paths:
root = Skpath( )
data_file = root / "data" / "file.txt"
iterdir()Iterate over directory contents.
path = Skpath( "src")
for item in path.iterdir():
print(item.name)
Returns
Generator[: Each item in the directory.
Raises
NotADirectoryError: If path is not a directory.
glob()Find paths matching a pattern.
path = Skpath( "src")
for py_file in path.glob("*.py"):
print(py_file.name)
Arguments
pattern: Glob pattern (ex. '*.txt').
strReturns
Generator[: Matching paths.
rglob()Recursively find paths matching a pattern.
root = Skpath( )
for py_file in root .rglob("*.py"):
print(py_file.rp )
Arguments
pattern: Glob pattern (ex. '*.py').
strReturns
Generator[: Matching paths in all subdirectories.
relative_to()Get path relative to another path.
path = Skpath( "src/utils/helpers.py")
rel = path.relative_to("src")
# Skpath("utils/helpers.py")
Arguments
other: Base path.
str | Path | Skpath Returns
: Relative path.
Raises
ValueError: If path is not relative to other.
with_name()Return path with changed name.
path = Skpath( "data/file.txt")
new_path = path.with_name("other.txt")
# Skpath("data/other.txt")
Arguments
name: New name.
strReturns
: Path with new name.
read_text() / write_text()Read and write text files (mirrors pathlib.Path).
path = Skpath( "data/config.json")
path.write_text("{}")
contents = path.read_text()
Arguments
write_text(data, encoding=None, errors=None, newline=None)
Returns
int: Number of characters written.
read_bytes() / write_bytes()Read and write binary files (mirrors pathlib.Path).
path = Skpath( "data/blob.bin")
path.write_bytes(b"\x00\x01")
data = path.read_bytes()
Returns
bytes: File contents.
with_stem()Return path with changed stem (filename without suffix).
path = Skpath( "data/file.txt")
new_path = path.with_stem("other")
# Skpath("data/other.txt")
Arguments
stem: New stem.
strReturns
: Path with new stem.
with_suffix()Return path with changed suffix.
path = Skpath( "data/file.txt")
new_path = path.with_suffix(".json")
# Skpath("data/file.json")
Arguments
suffix: New suffix (including dot).
strReturns
: Path with new suffix.
mkdir()Create directory.
path = Skpath( "new_dir")
path.mkdir()
# create parent directories
path = Skpath( "parent/child/grandchild")
path.mkdir(parents=True)
Arguments
mode: Directory permissions.
int = 0o777parents: Create parent directories.
bool = Falseexist_ok: Don't raise if directory exists.
bool = FalseRaises
FileExistsError: If directory exists and exist_ok=False. FileNotFoundError: If parent doesn't exist and parents=False.
touch()Create file or update timestamp.
path = Skpath( "new_file.txt")
path.touch()
Arguments
mode: File permissions.
int = 0o666exist_ok: Don't raise if file exists.
bool = Truermdir()Remove empty directory.
path = Skpath( "empty_dir")
path.rmdir()
Raises
OSError: If directory is not empty. NotADirectoryError: If path is not a directory.
unlink()Remove file or symbolic link.
path = Skpath( "file.txt")
path.unlink()
# don't raise if file doesn't exist
path.unlink(missing_ok=True)
Arguments
missing_ok: Don't raise if file doesn't exist.
bool = FalseRaises
FileNotFoundError: If file doesn't exist and missing_ok=False. IsADirectoryError: If path is a directory.
resolve()Return absolute path, resolving symlinks.
path = Skpath( "./relative/path")
resolved = path.resolve()
Returns
: Absolute path with symlinks resolved.
absolute()Return absolute version of path.
path = Skpath( "relative/path")
abs_path = path.absolute()
Returns
: Absolute path.
copy_to()Copy path to destination.
source = Skpath( "data/file.txt")
dest = source.copy_to("backup/file.txt")
# with options
dest = source.copy_to("backup/", overwrite=True, parents=True)
Arguments
destination: Target path or directory.
str | Path | Skpath overwrite: Remove existing destination.
bool = Falseparents: Create parent directories.
bool = TrueReturns
: Path to the copied file/directory.
Raises
FileNotFoundError: If source path doesn't exist. FileExistsError: If destination exists and overwrite=False.
move_to()Move path to destination.
source = Skpath( "temp/file.txt")
dest = source.move_to("data/file.txt")
# with options
dest = source.move_to("archive/", overwrite=True, parents=True)
Arguments
destination: Target path or directory.
str | Path | Skpath overwrite: Remove existing destination.
bool = Falseparents: Create parent directories.
bool = TrueReturns
: Path to the moved file/directory.
Raises
FileNotFoundError: If source path doesn't exist. FileExistsError: If destination exists and overwrite=False.
os.fspath Compatibility works with open(), os.path, and other functions that accept paths:
path = Skpath( "data/file.txt")
# works directly with open()
with open(path, 'r') as f:
content = f.read()
# works with os.path functions
import os
os.path.exists(path)
autopath DecoratorAutomatically converts path parameters based on type annotations.
from suitkaise .paths import autopath , AnyPath , Skpath
@autopath ()
def process(path: AnyPath ):
# path is guaranteed to be Skpath
return path.id
# works with any input type
process("src/main.py")
process(Path("src/main.py"))
process(Skpath( "src/main.py"))
Arguments
use_caller: Use caller's file path for missing parameters.
bool = Falsedebug: Print conversion messages.
bool = Falseonly: Only convert specific parameters.
str | list[str] | None = NoneThe decorator converts parameters based on their type annotations.
Supported types:
Skpath → converted to SkpathPath → normalized through Skpath, converted to Pathstr → normalized through Skpath, returns absolute path stringAnyPath (or any union containing Skpath) → converted to Skpath (richest type)str | Path (union without Skpath) → converted to PathSupported iterables:
list[Skpath ], list[Path], list[str]tuple[Skpath , ...], tuple[Path, ...], tuple[str, ...]set[Skpath ], set[Path], set[str]frozenset[Skpath ], frozenset[Path], frozenset[str]Iterable[Skpath ], Iterable[Path], Iterable[str] (converted to list)@autopath ()
def process(
path: Skpath ,
files: list[Path],
names: set[str],
):
...
use_caller OptionFill missing path parameters with the caller's file path.
@autopath (use_caller=True)
def log_from(path: Skpath = None):
print(f"Logging from: {path.rp }")
# called without argument - uses caller's file
log_from() # prints the file that called log_from()
only OptionRestrict conversion to specific parameters (faster for large lists).
@autopath (only="file_path")
def process(file_path: str, names: list[str], ids: list[str]):
# only file_path is normalized
# names and ids are left unchanged
return file_path
debug OptionPrint conversion messages.
@autopath (debug=True)
def process(path: Skpath ):
return path
process("src/main.py")
# @autopath: Converted path: str → Skpath
get_project_root ()Get the project root directory.
from suitkaise import paths
root = paths .get_project_root ()
# Skpath('/Users/me/myproject')
Arguments
expected_name: If provided, detected root must have this name.
str | None = NoneReturns
: Project root directory.
Raises
: If root cannot be detected or doesn't match expected name.
set_custom_root ()Override automatic root detection.
paths .set_custom_root ("/my/project")
Arguments
path: Path to use as project root.
str | PathRaises
: If path doesn't exist or isn't a directory.
get_custom_root ()Get the current custom root.
current = paths .get_custom_root ()
# "/my/project" or None
Returns
str | None: Custom root path or None.
clear_custom_root ()Revert to automatic root detection.
paths .clear_custom_root ()
CustomRoot Context ManagerTemporarily set a custom root:
with paths .CustomRoot( "/my/project"):
root = paths .get_project_root ()
# Skpath('/my/project')
# reverts to automatic detection after the block
get_caller_path ()Get the file path of the caller.
caller = paths .get_caller_path ()
Returns
: Caller's file path.
get_current_dir ()Get the directory containing the caller's file.
current_dir = paths .get_current_dir ()
Returns
: Caller's directory.
get_cwd ()Get the current working directory.
cwd = paths .get_cwd ()
Returns
: Current working directory.
get_module_path ()Get the file path where an object is defined.
from myapp import MyClass
path = paths .get_module_path (MyClass)
# Skpath pointing to the file where MyClass is defined
Arguments
obj: Object to inspect (module, class, function, etc.).
AnyReturns
: Module file path or None if not found.
get_project_paths ()Get all paths in the project.
# get all paths
all_paths = paths .get_project_paths ()
# use a custom root
all_paths = paths .get_project_paths (root="src")
# exclude specific paths
all_paths = paths .get_project_paths (exclude=["build", "dist"])
# get as strings for memory efficiency
all_paths = paths .get_project_paths (as_strings=True)
# ignore .*ignore files
all_paths = paths .get_project_paths (use_ignore_files=False)
Arguments
root: Custom root directory.
str | Path | Skpath | None = Noneexclude: Paths to exclude.
str | Path | Skpath | list[...] | None = Noneas_strings: Return string paths instead of Skpath objects.
bool = Falseuse_ignore_files: Respect .gitignore, .cursorignore, etc.
bool = TrueReturns
list[: All project paths.
get_project_structure ()Get a nested dict representing the project structure.
structure = paths .get_project_structure ()
# {
# "myproject": {
# "src": {
# "main.py": {},
# "utils.py": {}
# },
# "tests": {...}
# }
# }
Arguments
root: Custom root directory.
str | Path | Skpath | None = Noneexclude: Paths to exclude.
str | Path | Skpath | list[...] | None = Noneuse_ignore_files: Respect .gitignore, .cursorignore, etc.
bool = TrueReturns
dict: Nested dictionary of project structure.
get_formatted_project_tree ()Get a formatted tree string for the project structure.
tree = paths .get_formatted_project_tree ()
print(tree)
# myproject/
# ├── src/
# │ ├── main.py
# │ └── utils/
# └── tests/
# └── test_main.py
Arguments
root: Custom root directory.
str | Path | Skpath | None = Noneexclude: Paths to exclude.
str | Path | Skpath | list[...] | None = Noneuse_ignore_files: Respect .gitignore, .cursorignore, etc.
bool = Truedepth: Maximum depth to display (None = no limit).
int | None = Noneinclude_files: Include files in the tree.
bool = TrueReturns
str: Formatted tree string.
get_id ()Get the reversible encoded ID for a path.
path_id = paths .get_id ("myproject/feature/file.txt")
# "bXlwcm9qZWN0L2ZlYXR1cmUvZmlsZS50eHQ"
# same as
path_id = Skpath( "myproject/feature/file.txt").id
Arguments
path: Path to generate ID for.
str | Path | Skpath Returns
str: Base64url encoded ID.
is_valid_filename ()Check if a filename is valid across operating systems.
from suitkaise .paths import is_valid_filename
is_valid_filename ("my_file.txt") # True
is_valid_filename ("file<name>.txt") # False (contains <, >)
is_valid_filename ("CON") # False (Windows reserved)
is_valid_filename ("") # False (empty)
Arguments
filename: Filename to validate (not a full path).
strReturns
bool: True if valid, False otherwise.
streamline_path ()Sanitize a path by replacing invalid characters.
from suitkaise .paths import streamline_path
# basic cleanup
path = streamline_path ("My File<1>.txt", chars_to_replace=" ")
# "My_File_1_.txt"
# lowercase and limit length
path = streamline_path ("My Long Filename.txt", max_len=10, lowercase=True, chars_to_replace=" ")
# "my_long_fi.txt"
# replace invalid chars with custom character
path = streamline_path ("file:name.txt", replacement_char="-")
# "file-name.txt"
# ASCII only
path = streamline_path ("файл.txt", allow_unicode=False)
# "____.txt"
Arguments
path: Path or filename to sanitize.
strmax_len: Maximum length (suffix preserved, not counted).
int | None = Nonereplacement_char: Character to replace invalid chars with.
str = "_"lowercase: Convert to lowercase.
bool = Falsestrip_whitespace: Strip leading/trailing whitespace.
bool = Truechars_to_replace: Extra characters to replace.
str | list[str] | None = Noneallow_unicode: Allow unicode characters.
bool = TrueReturns
str: Sanitized path.
streamline_path_quick ()Simple version of that replaces all invalid and unicode characters.
from suitkaise import paths
path = paths .streamline_path_quick ("My File<1>файл.txt")
# "My_File_1_____.txt"
Arguments
path: Path or filename to sanitize.
strmax_len: Maximum length.
int | None = Nonereplacement_char: Character to replace invalid chars with.
str = "_"lowercase: Convert to lowercase.
bool = FalseReturns
str: Sanitized path.
PathDetectionError Raised when path or project root detection fails.
from suitkaise import paths
from suitkaise .paths import PathDetectionError
try:
root = paths .get_project_root ()
except PathDetectionError :
print("Could not detect project root")
Common causes:
NotAFileError Raised when a file operation is attempted on a directory.
from suitkaise .paths import Skpath , NotAFileError
path = Skpath( "some_directory")
try:
path.unlink() # attempting to unlink a directory
except NotAFileError :
print("Cannot unlink a directory")
AnyPath Type alias for parameters that accept any path type.
from suitkaise .paths import AnyPath
def process(path: AnyPath ) -> None:
# path can be str, Path, or Skpath
...
Note: does NOT include None. Use when None is acceptable.