paths actually works provides project-aware path handling, streamlining how you handle paths and ensuring cross-platform compatibility.
Skpath - enhanced path object with automatic project root detectionautopath - decorator for automatic path type conversionAnyPath - streamline path handling using this union typeAll paths use normalized separators (/) internally for cross-platform consistency.
Skpath Enhanced path object that wraps pathlib.Path.
Arguments
path: Path to wrap.
str | Path | Skpath | None = NoneNone, uses the caller's file pathReturns
: A new object.
When you create an , it automatically:
ap )rp )id )Skpath( "feature/file.txt") → detects root → computes ap, rp, id
defines a module-level threading.RLock (_skpath_lock) for potential thread-safe operations.
_path: Path The underlying pathlib.Path object. Always resolved to an absolute path.
_root: Path | None Cached project root. Lazily detected on first access.
_ap: str | None Cached absolute path with normalized separators. Lazily computed.
_rp: str | None Cached relative path to project root. Lazily computed.
_id: str | None Cached base64url encoded ID. Lazily computed.
_hash: int | None Cached hash value for use in sets and dicts.
__init__(path: str | Path | Skpath | None = None)The constructor handles four input types.
def __init__(self, path: str | Path | Skpath | None = None):
# initialize cached values
self._path: Path
self._root: Path | None = None
self._ap: str | None = None
self._rp: str | None = None
self._id: str | None = None
self._hash: int | None = None
if path is None:
# detect caller's file path using frame inspection
self._path = detect_caller_path()
elif isinstance(path, Skpath ):
# copy all values from source (avoids recomputation)
self._path = path._path
self._root = path._root
self._ap = path._ap
self._rp = path._rp
self._id = path._id
self._hash = path._hash
elif isinstance(path, Path):
# resolve to absolute path
self._path = path.resolve()
elif isinstance(path, str):
# try multiple interpretations
self._path = self._resolve_string_path(path)
If path is None: Uses detect_caller_path() which inspects the call stack to find the file that called . This allows to return a path to "this file" without passing any argument.
If path is : Copies all cached values directly. This is an optimization - if someone passes an existing Skpath, we don't recompute , , , etc.
If path is Path: Calls .resolve() to get an absolute path with symlinks resolved.
If path is str: Calls _resolve_string_path() which tries multiple interpretations (see below).
_resolve_string_path() tries multiple interpretations.
/ or \:ap (absolute path)Absolute path with normalized separators (/).
@property
def ap(self) -> str:
if self._ap is None:
self._ap = normalize_separators(str(self._path))
return self._ap
Always available, even for paths outside project root.
rp (relative path)Path relative to project root with normalized separators.
@property
def rp(self) -> str:
if self._rp is None:
self._rp = self._compute_rp()
return self._rp
def _compute_rp(self) -> str:
try:
root = self.root_path
rel_path = self._path.relative_to(root)
return normalize_separators(str(rel_path))
except (ValueError, PathDetectionError ):
return "" # outside project root
Returns
empty string if path is outside project root.
id (encoded ID)Reversible base64url encoded ID.
@property
def id (self) -> str:
if self._id is None:
path_to_encode = self.rp if self.rp else self.ap
self._id = encode_path_id(path_to_encode)
return self._id
Uses if available (for cross-platform compatibility), otherwise .
Can be used to reconstruct the path: .
root, root_str, root_pathProject root access in different formats.
root → Skpath objectroot_str → str with normalized separatorsroot_path → pathlib.Path object mirrors most pathlib.Path properties and methods, including file IO helpers like read_text(), write_text(), read_bytes(), and write_bytes().
Properties: name, stem, suffix, suffixes, parent, parents, parts, exists, is_file, is_dir, is_symlink, is_empty, stat, lstat
Methods: iterdir(), glob(), rglob(), relative_to(), with_name(), with_stem(), with_suffix(), mkdir(), touch(), rmdir(), unlink(), resolve(), absolute()
Additional methods: copy_to(), move_to() (with overwrite and parents options)
Additional properties:
as_dict: Dictionary representation with ap , rp , root, name, existsplatform : Absolute path with OS-native separators (backslash on Windows) supports the / operator for joining paths.
def __truediv__(self, other: str | Path | Skpath ) -> Skpath :
return Skpath( self._path / other_str)
Equality compares first (for cross-platform consistency), then falls back to .
def __eq__(self, other: Any) -> bool:
if self.rp and other_skpath.rp and self.rp == other_skpath.rp:
return True
return self.ap == other_skpath.ap
Hashing uses MD5 of (or if outside project root).
__fspath__ compatibility implements __fspath__() to work with open(), os.path, etc.:
def __fspath__(self) -> str:
return to_os_separators(self.ap)
Returns
OS-native separators (\ on Windows, / elsewhere).
Root detection walks up from a path looking for project indicators.
set_custom_root ())setup.sk file (suitkaise marker - highest priority)setup.py, setup.cfg, pyproject.toml.git, .gitignoreLICENSE, LICENSE.txt, etc. (case-insensitive)README.md, README.txt, etc. (case-insensitive)requirements.txt, etc.def _find_root_from_path(start_path: Path) -> Path | None:
# First pass: look for setup.sk specifically
check_path = current
while check_path != check_path.parent:
if (check_path / "setup.sk").exists():
return check_path
check_path = check_path.parent
# Second pass: look for any indicator
# Keep going up to find outermost root (handles nested projects)
check_path = current
best_root = None
while check_path != check_path.parent:
if _has_indicator(check_path):
best_root = check_path
check_path = check_path.parent
return best_root
Detected roots are cached to avoid repeated filesystem walks.
_cached_root: Path | None = None
_cached_root_source: Path | None = None # path used to detect cached root
Cache is invalidated when searching from a path outside the cached root.
Use clear_root_cache() to manually clear the cache.
: Override automatic detection.
: Get current custom root (or None).
: Revert to automatic detection.
: Context manager for temporary override.
All operations are thread-safe using threading.RLock.
autopath DecoratorDecorator that automatically converts path parameters based on type annotations.
Arguments
use_caller: If True, parameters that accept or Path will use the caller's file path if no value was provided.
bool = Falsedebug: If True, print messages when conversions occur.
bool = Falseonly: Only apply autopath to specific params.
str | list[str] | None = None@autopath ()
def process(path: Skpath ):
# path is guaranteed to be Skpath
...
# Equivalent to:
def process(path):
path = Skpath( path) # conversion happens here
...
The decorator recognizes:
Skpath , Path, strstr | Path | Skpath (AnyPath)list[Skpath ], tuple[Path, ...], set[str]For union types, it picks the richest type.
Skpath is in the union → convert to Skpath Path is in the union → convert to Pathstr is in the union → convert to strAll path-like inputs flow through for normalization.
input → Skpath → target type
This ensures:
/)def _convert_value(value, target_type, ...):
if target_type is Skpath :
return Skpath( value)
elif target_type is Path:
return Path(Skpath( value).ap)
elif target_type is str:
return Skpath( value).ap
use_caller optionWhen use_caller=True, missing path parameters are filled 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() # logs the file that called log_from()
only optionRestrict conversion to specific parameters.
@autopath (only="file_path")
def process(file_path: str, names: list[str]):
# only file_path is normalized
# names is left unchanged (faster for large lists)
...
get_project_root ()Get the project root directory.
def get_project_root (expected_name: str | None = None) -> Skpath :
root_path = detect_project_root(expected_name=expected_name)
return Skpath( root_path)
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.
get_caller_path ()Get the file path of the caller.
def get_caller_path () -> Skpath :
caller = detect_caller_path(skip_frames=1)
return Skpath( caller)
Uses detect_caller_path() which inspects the call stack, skipping internal frames to find the actual caller.
Returns
: Caller's file path.
Raises
: If caller detection fails.
get_current_dir ()Get the directory containing the caller's file.
def get_current_dir () -> Skpath :
caller = detect_caller_path(skip_frames=1)
return Skpath( caller.parent)
Returns
: Caller's directory.
get_cwd ()Get the current working directory.
def get_cwd () -> Skpath :
return Skpath( get_cwd_path())
Uses Path.cwd() internally.
Returns
: Current working directory.
get_module_path ()Get the file path where an object is defined.
def get_module_path (obj: Any) -> Skpath | None:
path = get_module_file_path(obj)
if path is None:
return None
return Skpath( path)
Arguments
obj: Object to inspect (module, class, function, etc.).
AnyThe function handles:
__file__ attribute__file____module__: Gets the module, then uses __file__Returns
: Module file path, or None if not found.
Raises
ImportError: If obj is a module name string that cannot be imported.
get_id ()Get the reversible encoded ID for a path.
def get_id (path: str | Path | Skpath ) -> str:
if isinstance(path, Skpath ):
return path.id
return Skpath( path).id
Arguments
path: Path to generate ID for.
str | Path | Skpath Returns
str: Base64url encoded ID.
get_project_paths ()Get all paths in the project.
def get_project_paths (
root: str | Path | Skpath | None = None,
exclude: str | Path | Skpath | list[...] | None = None,
as_strings: bool = False,
use_ignore_files: bool = True,
) -> list[Skpath ] | list[str]:
return _get_project_paths(
root=root,
exclude=exclude,
as_strings=as_strings,
use_ignore_files=use_ignore_files,
)
Arguments
root: Custom root directory (defaults to detected project root).
str | Path | Skpath | None = Noneexclude: Paths to exclude (single path or list).
str | Path | Skpath | list[...] | None = Noneas_strings: Return string paths instead of Skpath objects.
bool = Falseuse_ignore_files: Respect .gitignore, .cursorignore, etc.
bool = TrueThe function:
.*ignore patterns (if enabled)Returns
list[: All project paths.
Raises
: If project root cannot be detected.
get_project_structure ()Get a nested dict representing the project structure.
def get_project_structure (
root: str | Path | Skpath | None = None,
exclude: str | Path | Skpath | list[...] | None = None,
use_ignore_files: bool = True,
) -> dict:
return _get_project_structure(
root=root,
exclude=exclude,
use_ignore_files=use_ignore_files,
)
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
a nested dict where:
{
"myproject": {
"src": {
"main.py": {},
"utils.py": {}
},
"tests": {...}
}
}
Returns
dict: Nested dictionary of project structure.
Raises
: If project root cannot be detected.
get_formatted_project_tree ()Get a formatted tree string for the project structure.
def get_formatted_project_tree (
root: str | Path | Skpath | None = None,
exclude: str | Path | Skpath | list[...] | None = None,
use_ignore_files: bool = True,
depth: int | None = None,
include_files: bool = True,
) -> str:
return _get_formatted_project_tree(
root=root,
exclude=exclude,
use_ignore_files=use_ignore_files,
depth=depth,
include_files=include_files,
)
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 = TrueUses box-drawing characters (│, ├─, └─) to create visual hierarchy:
myproject/
├── src/
│ ├── main.py
│ └── utils/
└── tests/
└── test_main.py
Returns
str: Formatted tree string.
Raises
: If project root cannot be detected.
Path IDs use base64url encoding for safe transport.
def encode_path_id(path_str: str) -> str:
# normalize path separators first
normalized = normalize_separators(path_str)
encoded = base64.urlsafe_b64encode(normalized.encode("utf-8"))
# remove padding for cleaner IDs
return encoded.decode("utf-8").rstrip("=")
def decode_path_id(encoded_id: str) -> str | None:
try:
# add back padding if needed
padding = 4 - (len(encoded_id) % 4)
if padding != 4:
encoded_id += "=" * padding
decoded = base64.urlsafe_b64decode(encoded_id.encode("utf-8"))
return decoded.decode("utf-8")
except Exception:
return None
The encoding is:
- and _ instead of + and /)/ before encodingis_valid_filename ()Arguments
filename: Filename to validate.
strReturns
bool: True if valid, False otherwise.
Checks if a filename is valid across common operating systems.
<>:"/\|?*\0\t\n\rCON, PRN, AUX, NUL, COM1-9, LPT1-9streamline_path ()Sanitizes a path by replacing invalid characters.
Arguments
path: Path to sanitize.
strmax_len: Maximum length to truncate to.
int | None = Nonereplacement_char: Character to replace invalid characters with.
str = "_"lowercase: Convert to lowercase.
bool = Falsestrip_whitespace: Strip whitespace.
bool = Truechars_to_replace: Extra characters to replace.
str | list[str] | None = Noneallow_unicode: Allow unicode characters.
bool = TrueReturns
str: Sanitized path.
allow_unicode=False)streamline_path_quick ()Simple version of with common defaults.
def streamline_path_quick (
path: str,
max_len: int | None = None,
replacement_char: str = "_",
lowercase: bool = False
) -> str:
return streamline_path (
path,
max_len=max_len,
replacement_char=replacement_char,
lowercase=lowercase,
strip_whitespace=True,
chars_to_replace=" ",
allow_unicode=False,
)
Arguments
path: Path 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.
This version:
PathDetectionError Raised when path or project root detection fails.
Examples:
NotAFileError Raised when a file operation is attempted on a directory.
Example: Calling on a directory.
AnyPath Type alias for path parameters that accept multiple types.
from typing import Union
# using Union for forward reference compatibility at runtime
AnyPath = Union[str, Path, "Skpath"]
Note: Does NOT include None - use when None is acceptable.
Use in function annotations to indicate a parameter accepts any path type:
def process(path: AnyPath ) -> None:
...
When used with , parameters annotated with are converted to (the richest type in the union).
Module-level state is protected by threading.RLock instances.
_root_lock: Protects custom root state (_custom_root)_cache_lock: Protects cached root state (_cached_root, _cached_root_source)_skpath_lock: Defined for potential Skpath operations (currently unused)_autopath_lock: Defined for potential autopath operations (currently unused)_id_lock: Defined in id_utils for potential ID operations (currently unused)RLock (reentrant lock) is used because operations may call each other (e.g., detect_project_root() is called from both and custom root validation).
The root detection functions (, , , detect_project_root) actively use locks to protect shared state.