"""Path Module."""
from __future__ import annotations
__all__ = (
"FileConfig",
"FrameSimple",
"Passwd",
"Path",
"PathStat",
"toiter",
"AnyPath",
)
import contextlib
import dataclasses
import grp
import hashlib
import os
import pathlib
import pickle
import pwd
import stat
import subprocess
import sys
import sysconfig
import tempfile
import tokenize
from collections.abc import Iterable
from typing import IO, TYPE_CHECKING, Any, AnyStr, TypeAlias, cast
from .constants import MACOS, SUDO, USER
from .errors import InvalidArgumentError
from .typings import StrOrBytesPath
if TYPE_CHECKING:
import configparser
import types
[docs]
@dataclasses.dataclass
class FileConfig:
"""FileConfig class."""
file: Path | None = None
config: dict | configparser.ConfigParser = dataclasses.field(default_factory=dict)
[docs]
@dataclasses.dataclass
class FrameSimple:
"""Simple frame class."""
back: types.FrameType
code: types.CodeType
frame: types.FrameType
function: str
globals: dict[str, Any] # noqa: A003, A003
lineno: int
locals: dict[str, Any] # noqa: A003
name: str
package: str
path: Path
vars: dict[str, Any] # noqa: A003
[docs]
@dataclasses.dataclass
class Passwd:
"""Passwd class from either `uid` or `user`.
Args:
-----
uid: int
User ID
user: str
Username
Attributes:
-----------
gid: int
Group ID
gecos: str
Full name
group: str
Group name
groups: tuple(str)
Groups list
home: Path
User's home
shell: Path
User shell
uid: int
User ID (default: :func:`os.getuid` current user id)
user: str
Username
"""
data: dataclasses.InitVar[Passwd | AnyPath | str | int] = USER
gid: int = dataclasses.field(default=None, init=False)
gecos: str = dataclasses.field(default=None, init=False)
group: str = dataclasses.field(default=None, init=False)
groups: dict[str, int] = dataclasses.field(default=None, init=False)
home: Path = dataclasses.field(default=None, init=False)
shell: Path = dataclasses.field(default=None, init=False)
uid: int = dataclasses.field(default=None, init=False)
user: str = dataclasses.field(default=None, init=False)
def __post_init__(self, data=USER):
"""Instance of :class:`nodeps:Passwd` from either `uid` or `user` (default: :func:`os.getuid`).
Uses completed/real id's (os.getgid, os.getuid) instead effective id's (os.geteuid, os.getegid) as default.
- UID and GID: when login from $LOGNAME, $USER or os.getuid()
- RUID and RGID: completed real user id and group id inherit from UID and GID
(when completed start EUID and EGID and set to the same values as RUID and RGID)
- EUID and EGID: if executable has 'setuid' or 'setgid' (i.e: ping, sudo), EUID and EGID are changed
to the owner (setuid) or group (setgid) of the binary.
- SUID and SGID: if executable has 'setuid' or 'setgid' (i.e: ping, sudo), SUID and SGID are saved with
RUID and RGID to do unprivileged tasks by a privileged completed (had 'setuid' or 'setgid').
Can not be accessed in macOS with `os.getresuid()` and `os.getresgid()`
Examples:
>>> import pathlib
>>> from nodeps import MACOS, USER, SUDO
>>> from nodeps import Passwd
>>> from nodeps import Path
>>>
>>> default = Passwd()
>>> login = Passwd.from_login()
>>>
>>> assert default == Passwd(Path()) == Passwd(pathlib.Path()) == Passwd() == Passwd(os.getuid()) == \
login
>>> if SUDO:
... assert default != Passwd().from_root()
... else:
... assert default == Passwd().from_root()
>>> assert default.gid == os.getgid()
>>> if home := os.environ.get("HOME"):
... assert default.home == Path(home)
>>> if shell := os.environ.get("SHELL"):
... assert default.shell == Path(shell)
>>> assert default.uid == os.getuid()
>>> assert default.user == USER
>>> if MACOS:
... assert "staff" in default.groups
... assert "admin" in default.groups
Errors:
os.setuid(0)
os.seteuid(0)
os.setreuid(0, 0)
os.getuid()
os.geteuid(
os.setuid(uid) can only be used if running as root in macOS.
os.seteuid(euid) -> 0
os.setreuid(ruid, euid) -> sets EUID and RUID (probar con 501, 0)
os.setpgid(os.getpid(), 0) -> sets PGID and RGID (probar con 501, 0)
Returns:
Instance of :class:`nodeps:Passwd`
"""
if isinstance(data, self.__class__):
self.__dict__.update(data.__dict__)
return
if (isinstance(data, str) and not data.isnumeric()) or isinstance(data, pathlib.PurePosixPath):
passwd = pwd.getpwnam(cast(str, getattr(data, "owner", lambda: None)() or data))
else:
passwd = pwd.getpwuid(int(data) if data or data == 0 else os.getuid())
self.gid = passwd.pw_gid
self.gecos = passwd.pw_gecos
self.home = Path(passwd.pw_dir)
self.shell = Path(passwd.pw_shell)
self.uid = passwd.pw_uid
self.user = passwd.pw_name
group = grp.getgrgid(self.gid)
self.group = group.gr_name
self.groups = {grp.getgrgid(gid).gr_name: gid for gid in os.getgrouplist(self.user, self.gid)}
@property
def is_su(self):
"""Returns True if login as root, uid=0 and not `SUDO_USER`."""
return self.uid == 0 and not bool(os.environ.get("SUDO_USER"))
@property
def is_sudo(self):
"""Returns True if SUDO_USER is set."""
return bool(os.environ.get("SUDO_USER"))
@property
def is_user(self):
"""Returns True if user and not `SUDO_USER`."""
return self.uid != 0 and not bool(os.environ.get("SUDO_USER"))
[docs]
@classmethod
def from_login(cls):
"""Returns instance of :class:`nodeps:Passwd` from '/dev/console' on macOS and `os.getlogin()` on Linux."""
try:
user = Path("/dev/console").owner() if MACOS else os.getlogin()
except (OSError, FileNotFoundError):
user = p.owner() if (p := Path("/proc/self/loginuid")).exists() else USER
return cls(user)
[docs]
@classmethod
def from_sudo(cls):
"""Returns instance of :class:`nodeps:Passwd` from `SUDO_USER` if set or current user."""
uid = os.environ.get("SUDO_UID", os.getuid())
return cls(uid)
[docs]
@classmethod
def from_root(cls):
"""Returns instance of :class:`nodeps:Passwd` for root."""
return cls(0)
[docs]
class Path(pathlib.Path, pathlib.PurePosixPath):
"""Path helper class."""
def __call__(
self,
name="",
file="is_dir",
passwd=None,
mode=None,
effective_ids=True,
follow_symlinks=False,
):
"""Make dir or touch file and create subdirectories as needed.
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempdir() as t:
... p = t('1/2/3/4')
... assert p.is_dir() is True
... p = t('1/2/3/4/5/6/7.py', file="is_file")
... assert p.is_file() is True
... t('1/2/3/4/5/6/7.py/8/9.py', file="is_file") # doctest: +IGNORE_EXCEPTION_DETAIL, +ELLIPSIS
Traceback (most recent call last):
NotADirectoryError: File: ...
Args:
name: path to add.
file: file or directory.
passwd: user.
mode: mode.
effective_ids: If True, access will use the effective uid/gid instead of
follow_symlinks: resolve self if self is symlink (default: True).
Returns:
Path.
"""
# noinspection PyArgumentList
return (self.mkdir if file in ["is_dir", "exists"] else self.touch)(
name=name,
passwd=passwd,
mode=mode,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
)
def __contains__(self, value):
"""Checks all items in value exist in self.resolve().
To check only parts use self.has.
Examples:
>>> from nodeps import Path
>>> from nodeps import USER
>>>
>>> assert '/usr' in Path('/usr/local')
>>> assert 'usr local' in Path('/usr/local')
>>> assert 'home' not in Path('/usr/local')
>>> assert '' not in Path('/usr/local')
>>> assert '/' in Path()
>>> assert USER in Path.home()
Args:
value: space separated list of items to check, or iterable of items.
Returns:
bool
"""
value = self.__class__(value) if isinstance(value, str) and "/" in value else toiter(value)
return all(item in self.resolve().parts for item in value)
def __eq__(self, other):
"""Equal based on parts.
Examples:
>>> from nodeps import Path
>>>
>>> assert Path('/usr/local') == Path('/usr/local')
"""
if not isinstance(other, self.__class__):
return NotImplemented
return tuple(self.parts) == tuple(other.parts)
def __hash__(self):
"""Hash based on parts."""
return self._hash if hasattr(self, "_hash") else hash(tuple(self.parts))
def __iter__(self):
"""Iterate over path parts.
Examples:
>>> from nodeps import Path
>>>
>>> assert list(Path('/usr/local')) == ['/', 'usr', 'local',]
Returns:
Iterable of path parts.
"""
return iter(self.parts)
def __lt__(self, other):
"""Less than based on parts."""
if not isinstance(other, self.__class__):
return NotImplemented
return self.parts < other.parts
def __le__(self, other):
"""Less than or equal based on parts."""
if not isinstance(other, self.__class__):
return NotImplemented
return self.parts <= other.parts
def __gt__(self, other):
"""Greater than based on parts."""
if not isinstance(other, self.__class__):
return NotImplemented
return self.parts > other.parts
def __ge__(self, other):
"""Greater than or equal based on parts."""
if not isinstance(other, self.__class__):
return NotImplemented
return self.parts >= other.parts
[docs]
def access(
self,
os_mode=os.W_OK,
*,
dir_fd=None,
effective_ids=True,
follow_symlinks=False,
):
# noinspection LongLine
"""Checks if file or directory exists and has access (returns None if file/directory does not exist.
Use the real uid/gid to test for access to a path `Real Effective IDs.`_.
- real: user owns the completed.
- effective: user invoking.
Examples:
>>> import os
>>> from nodeps import Path
>>> from nodeps import MACOS
>>> from nodeps import LOCAL
>>> from nodeps import USER
>>>
>>> assert Path().access() is True
>>> if USER == "root":
... assert Path('/usr/bin').access() is True
... else:
... assert Path('/usr/bin').access() is False
>>> assert Path('/tmp').access(follow_symlinks=True) is True
>>> assert Path('/tmp').access(effective_ids=False, follow_symlinks=True) is True
>>> if MACOS and LOCAL:
... assert Path('/etc/bashrc').access(effective_ids=False) is False
... assert Path('/etc/sudoers').access(effective_ids=False, os_mode=os.R_OK) is False
Args:
os_mode: Operating-system mode bitfield. Can be F_OK to test existence,
or the inclusive-OR of R_OK, W_OK, and X_OK (default: `os.W_OK`).
dir_fd: If not None, it should be a file descriptor open to a directory,
and path should be relative; path will then be relative to that
directory.
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: True).
follow_symlinks: If False, and the last element of the path is a symbolic link,
access will examine the symbolic link itself instead of the file
the link points to (default: False).
Note:
Most operations will use the effective uid/gid (what the operating system
looks at to make a decision whether you are allowed to do something), therefore this
routine can be used in a suid/sgid environment to test if the invoking user
has the specified access to the path.
When a setuid program (`-rwsr-xr-x`) executes, the completed changes its Effective User ID (EUID)
from the default RUID to the owner of this special binary executable file:
- euid: owner of executable (`os.geteuid()`).
- uid: user starting the completed (`os.getuid()`).
Returns:
True if access.
See Also:
`Real Effective IDs.
<https://stackoverflow.com/questions/32455684/difference-between-real-user-id-effective-user-id-and-saved
-user-id>`_
"""
if not self.exists():
return None
return os.access(
self,
mode=os_mode,
dir_fd=dir_fd,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
)
[docs]
def add(self, *args, exception=False):
"""Add args to self.
Examples:
>>> from nodeps import Path
>>> import nodeps
>>>
>>> p = Path().add('a/a')
>>> assert Path() / 'a/a' == p
>>> p = Path().add(*['a', 'a'])
>>> assert Path() / 'a/a' == p
>>> p = Path(nodeps.__file__)
>>> p.add('a', exception=True) # doctest: +IGNORE_EXCEPTION_DETAIL, +ELLIPSIS
Traceback (most recent call last):
FileNotFoundError...
Args:
*args: parts to be added.
exception: raise exception if self is not dir and parts can not be added (default: False).
Raises:
FileNotFoundError: if self is not dir and parts can not be added.
Returns:
Compose path.
"""
if exception and self.is_file() and args:
msg = f"parts: {args}, can not be added since path is file or not directory: {self}"
raise FileNotFoundError(msg)
args = toiter(args)
path = self
for arg in args:
path = path / arg
return path
[docs]
def append_text(self, text, encoding=None, errors=None):
"""Open the file in text mode, append to it, and close the file (creates file if not file).
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempfile() as tmp:
... _ = tmp.write_text('Hello')
... assert 'Hello World!' in tmp.append_text(' World!')
Args:
text: text to add.
encoding: encoding (default: None).
errors: raise error if there is no file (default: None).
Returns:
File text with text appended.
"""
if not isinstance(text, str):
msg = f"data must be str, not {text.__class__.__name__}"
raise TypeError(msg)
with self.open(mode="a", encoding=encoding, errors=errors) as f:
f.write(text)
return self.read_text()
[docs]
@contextlib.contextmanager
def cd(self):
"""Change dir context manager to self if dir or parent if file and exists.
Examples:
>>> from nodeps import Path
>>>
>>> new = Path('/usr/local')
>>> p = Path.cwd()
>>> with new.cd() as prev:
... assert new == Path.cwd()
... assert prev == p
>>> assert p == Path.cwd()
Returns:
Old Pwd Path.
"""
oldpwd = self.cwd()
try:
self.chdir()
yield oldpwd
finally:
oldpwd.chdir()
[docs]
def chdir(self):
"""Change to self if dir or file parent if file and file exists.
Examples:
>>> from nodeps import Path
>>>
>>> new = Path(__file__).chdir()
>>> assert new == Path(__file__).parent
>>> assert Path.cwd() == new
>>>
>>> new = Path(__file__).parent
>>> assert Path.cwd() == new
>>>
>>> Path("/tmp/foo").chdir() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
FileNotFoundError: ... No such file or directory: '/tmp/foo'
Raises:
FileNotFoundError: No such file or directory if path does not exist.
Returns:
Path with changed directory.
"""
path = self.to_parent()
os.chdir(path)
return path
[docs]
def checksum(self, algorithm="sha256", block_size=65536):
"""Calculate the checksum of a file.
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempfile() as tmp:
... _ = tmp.write_text('Hello')
... assert tmp.checksum() == '185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969'
Args:
algorithm: hash algorithm (default: 'sha256').
block_size: block size (default: 65536).
Returns:
Checksum of file.
"""
sha = hashlib.new(algorithm)
with self.open("rb") as f:
for block in iter(lambda: f.read(block_size), b""):
sha.update(block)
return sha.hexdigest()
[docs]
def chmod(
self,
mode=None,
effective_ids=True,
exception=True,
follow_symlinks=False,
recursive=False,
):
"""Change mode of self.
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempfile() as tmp:
... changed = tmp.chmod(777)
... assert changed.stat().st_mode & 0o777 == 0o777
... assert changed.stats().mode == "-rwxrwxrwx"
... assert changed.chmod("o-x").stats().mode == '-rwxrwxrw-'
>>>
>>> Path("/tmp/foo").chmod() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
FileNotFoundError: ... No such file or directory: '/tmp/foo'
Raises:
FileNotFoundError: No such file or directory if path does not exist and exception is True.
Args:
mode: mode to change to (default: None).
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: False).
follow_symlinks: resolve self if self is symlink (default: True).
exception: raise exception if self does not exist (default: True).
recursive: change owner of self and all subdirectories (default: False).
Returns:
Path with changed mode.
"""
if exception and not self.exists():
msg = f"path does not exist: {self}"
raise FileNotFoundError(msg)
subprocess.run(
[
*self.sudo(
force=True,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
),
f"{self.chmod.__name__}",
*(["-R"] if recursive and self.is_dir() else []),
str(mode or (755 if self.is_dir() else 644)),
self.resolve() if follow_symlinks else self,
],
capture_output=True,
)
return self
[docs]
def chown(
self,
passwd=None,
effective_ids=True,
exception=True,
follow_symlinks=False,
recursive=False,
):
"""Change owner of path.
Examples:
>>> from nodeps import Path
>>> from nodeps import Passwd
>>> from nodeps import MACOS
>>>
>>> with Path.tempfile() as tmp:
... changed = tmp.chown(passwd=Passwd.from_root())
... st = changed.stat()
... assert st.st_gid == 0
... assert st.st_uid == 0
... stats = changed.stats()
... assert stats.gid == 0
... assert stats.uid == 0
... assert stats.user == "root"
... if MACOS:
... assert stats.group == "wheel"
... g = "admin"
... else:
... assert stats.group == "root"
... g = "adm"
... changed = tmp.chown(f"{os.getuid()}:{g}")
... stats = changed.stats()
... assert stats.group == g
... assert stats.uid == os.getuid()
>>>
>>> Path("/tmp/foo").chown() # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
FileNotFoundError: ... No such file or directory: '/tmp/foo'
Raises:
FileNotFoundError: No such file or directory if path does not exist and exception is True.
ValueError: passwd must be string with user:group.
Args:
passwd: user/group passwd to use, or string with user:group (default: None for USER).
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: False).
exception: raise exception if self does not exist (default: True).
follow_symlinks: resolve self if self is symlink (default: True).
recursive: change owner of self and all subdirectories (default: False).
Returns:
Path with changed owner.
"""
if exception and not self.exists():
msg = f"path does not exist: {self}"
raise FileNotFoundError(msg)
if isinstance(passwd, str) and ":" in passwd:
pass
else:
passwd = Passwd(passwd or USER)
subprocess.run(
[
*self.sudo(
force=True,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
),
f"{self.chown.__name__}",
*(["-R"] if recursive and self.is_dir() else []),
f"{passwd.user}:{passwd.group}" if isinstance(passwd, Passwd) else passwd,
self.resolve() if follow_symlinks else self,
],
check=True,
capture_output=True,
)
return self
[docs]
def cmp(self, other):
"""Determine, whether two files provided to it are the same or not.
By the same means that their contents are the same or not (excluding any metadata).
Uses Cryptographic Hashes (using SHA256 - Secure hash algorithm 256) as a hash function.
Examples:
>>> from nodeps import Path
>>> import nodeps
>>> import asyncio
>>>
>>> assert Path(nodeps.__file__).cmp(nodeps.__file__) is True
>>> assert Path(nodeps.__file__).cmp(asyncio.__file__) is False
Args:
other: other file to compare to
Returns:
True if equal.
"""
return self.checksum() == self.__class__(other).checksum()
[docs]
def cp(
self,
dest,
contents=False,
effective_ids=True,
follow_symlinks=False,
preserve=False,
):
"""Wrapper for shell `cp` command to copy file recursivily and adding sudo if necessary.
Examples:
>>> import sys
>>> from nodeps import Path
>>> from nodeps import SUDO, USER
>>> from nodeps import Passwd
>>>
>>> with Path.tempfile() as tmp:
... changed = tmp.chown(passwd=Passwd.from_root())
... copied = Path(__file__).cp(changed)
... st = copied.stat()
... assert st.st_gid == 0
... assert st.st_uid == 0
... stats = copied.stats()
... assert "-rw-" in stats.mode
... _ = tmp.chown()
... assert copied.cmp(__file__)
>>> with Path.tempdir() as tmp:
... _ = tmp.chmod("go+rx")
... _ = tmp.chown(passwd=Passwd.from_root())
... src = Path(__file__).parent
... dirname = src.name
... filename = Path(__file__).name
...
... _ = src.cp(tmp)
... destination = tmp / dirname
... stats = destination.stats()
... assert stats.mode == "drwxr-xr-x"
... file = destination / filename
... st = file.stat()
... assert st.st_gid == 0
... assert st.st_uid == 0
... assert file.owner() == "root"
... tmp = tmp.chown(recursive=True)
... assert file.owner() == USER
... assert file.cmp(__file__)
...
... _ = src.cp(tmp, contents=True)
... file = tmp / filename
... assert file.cmp(__file__)
>>>
>>> Path("/tmp/foo").cp("/tmp/boo") # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
FileNotFoundError: ... No such file or directory: '/tmp/foo'
Args:
dest: destination.
contents: copy contents of self to dest, `cp src/ dest` instead of `cp src dest` (default: False)`.
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: False).
follow_symlinks: '-P' the 'cp' default, no symlinks are followed,
all symbolic links are followed when True '-L' (actual files are copyed but if there are existing links
will be left them untouched) (default: False)
`-H` cp option is not implemented (default: False).
preserve: preserve file attributes (default: False).
Raises:
FileNotFoundError: No such file or directory if path does not exist.
Returns:
Dest.
"""
dest = self.__class__(dest)
if not self.exists():
msg = f"path does not exist: {self}"
raise FileNotFoundError(msg)
source = [self]
if contents and self.is_dir() and len(files := list(self.iterdir())) > 0:
# GNU: cp file/ /tmp -> does not copy the contents.
source = files
subprocess.run(
[
*dest.sudo(effective_ids=effective_ids, follow_symlinks=follow_symlinks),
f"{self.cp.__name__}",
*(["-R"] if self.is_dir() else []),
*(["-L"] if follow_symlinks else []),
*(["-p"] if preserve else []),
*source,
dest,
],
check=True,
capture_output=True,
)
return dest
# noinspection PyMethodOverriding
[docs]
def exists(self):
"""Check if file exists or is a broken link (super returns False if it is a broken link, we return True).
Examples:
>>> from nodeps import Path
>>>
>>> Path(__file__).exists()
True
>>> with Path.tempcd() as tmp:
... source = tmp.touch(tmp / "source")
... destination = source.ln(tmp / "destination")
... assert destination.is_symlink()
... source.unlink()
... assert destination.exists()
... assert not pathlib.Path(destination).exists()
Returns:
True if file exists or is broken link.
"""
if super().exists():
return True
return self.is_symlink()
[docs]
@classmethod
def expandvars(cls, path=None):
"""Return a Path instance from expanded environment variables in path.
Expand shell variables of form $var and ${var}.
Unknown variables are left unchanged.
Examples:
>>> from nodeps import Path
>>>
>>> Path.expandvars('~/repo') # doctest: +ELLIPSIS
Path('~/repo')
>>> Path.expandvars('${HOME}/repo') # doctest: +ELLIPSIS
Path('.../repo')
Returns:
Expanded Path.
"""
return cls(os.path.expandvars(path) if path is not None else "")
[docs]
def file_in_parents(self, exception=True, follow_symlinks=False):
"""Find up until file with name is found.
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempfile() as tmpfile:
... new = tmpfile / "sub" / "file.py"
... assert new.file_in_parents(exception=False) == tmpfile.absolute()
>>>
>>> with Path.tempdir() as tmpdir:
... new = tmpdir / "sub" / "file.py"
... assert new.file_in_parents() is None
Args:
exception: raise exception if a file is found in parents (default: False).
follow_symlinks: resolve self if self is symlink (default: True).
Raises:
NotADirectoryError: ... No such file or directory: '/tmp/foo'
Returns:
File found in parents (str) or None
"""
path = self.resolve() if follow_symlinks else self
start = path
while True:
if path.is_file():
if exception:
msg = f"File: {path} found in path: {start}"
raise NotADirectoryError(msg)
return path
if path.is_dir() or (
path := path.parent.resolve() if follow_symlinks else path.parent.absolute()
) == self.__class__("/"):
return None
[docs]
def find_up(self, uppermost=False):
"""Find file or dir up.
Examples:
>>> import email.mime.application
>>> import email
>>> import email.mime
>>> from nodeps import Path
>>>
>>> assert 'email/mime/__init__.py' in Path(email.mime.__file__, "__init__.py").find_up()
>>> assert 'email/__init__.py' in Path(email.__file__, "__init__.py").find_up(uppermost=True)
Args:
uppermost: find uppermost (default: False).
Returns:
FindUp:
"""
start = self.absolute().parent
latest = None
found = None
while True:
find = start / self.name
if find.exists():
found = find
if not uppermost:
return find
latest = find
start = start.parent
if start == Path("/"):
return latest if latest is not None and latest.exists() else found
[docs]
def has(self, value):
"""Checks all items in value exist in `self.parts` (not absolute and not relative).
Only checks parts and not resolved as checked by __contains__ or absolute.
Examples:
>>> from nodeps import Path
>>>
>>> assert Path('/usr/local').has('/usr') is True
>>> assert Path('/usr/local').has('usr local') is True
>>> assert Path('/usr/local').has('home') is False
>>> assert Path('/usr/local').has('') is False
Args:
value: space separated list of items to check, or iterable of items.
Returns:
bool
"""
value = self.__class__(value) if isinstance(value, str) and "/" in value else toiter(value)
return all(item in self.parts for item in value)
[docs]
def installed(self):
"""Check if file is installed.
Examples:
>>> import pytest
>>> from nodeps import Path
>>>
>>> assert Path(pytest.__file__).installed() is True
"""
return self.is_relative_to(self.purelib())
[docs]
def ln(self, dest, force=True):
"""Wrapper for super `symlink_to` to return the new path and changing the argument.
If symbolic link already exists and have the same source, it will not be overwritten.
Similar:
- dest.symlink_to(src)
- src.ln(dest) -> dest
- os.symlink(src, dest)
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempcd() as tmp:
... source = tmp.touch("source")
... _ = source.ln(tmp / "destination")
... destination = source.ln(tmp / "destination")
... assert destination.is_symlink()
... assert destination.resolve() == source.resolve()
... assert destination.readlink().resolve() == source.resolve()
...
... touch = tmp.touch(tmp / "touch")
... _ = tmp.ln(tmp / "destination", force=False) # doctest: +IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
FileExistsError:
Raises:
FileExistsError: if dest already exists or is a symbolic link with different source and force is False.
Args:
dest: link destination (ln -s self dest)
force: force creation of link, if file or link exists and is different (default: True)
"""
dest = self.__class__(dest)
if dest.is_symlink() and dest.readlink().resolve() == self.resolve():
return dest
if force and dest.exists():
dest.rm()
os.symlink(self, dest)
return dest
[docs]
def ln_rel(self, dest):
"""Create a symlink pointing to ``target`` from ``location``.
Args:
dest: The location of the symlink itself.
"""
# HACER: examples and check if exists and merge with ln with absolute argument.
target = self
destination = self.__class__(dest)
target_dir = destination.parent
target_dir.mkdir()
relative_source = os.path.relpath(target, target_dir)
dir_fd = os.open(str(target_dir.absolute()), os.O_RDONLY)
print(f"{relative_source} -> {destination.name} in {target_dir}")
try:
os.symlink(relative_source, destination.name, dir_fd=dir_fd)
finally:
os.close(dir_fd)
return destination
[docs]
def mkdir(
self,
name="",
passwd=None,
mode=None,
effective_ids=True,
follow_symlinks=False,
):
"""Add directory, make directory, change mode and return new Path.
Examples:
>>> import getpass
>>> from nodeps import Path
>>> from nodeps import Passwd
>>>
>>> with Path.tempcd() as tmp:
... directory = tmp('1/2/3/4')
... assert directory.is_dir() is True
... assert directory.owner() == getpass.getuser()
...
... _ = directory.chown(passwd=Passwd.from_root())
... assert directory.owner() == "root"
... five = directory.mkdir("5")
... assert five.text.endswith('/1/2/3/4/5') is True
... assert five.owner() == "root"
...
... six = directory("6")
... assert six.owner() == "root"
...
... seven = directory("7", passwd=Passwd())
... assert seven.owner() == getpass.getuser()
...
... _ = directory.chown(passwd=Passwd())
Args:
name: name.
passwd: group/user for chown, if None ownership will not be changed (default: None).
mode: mode.
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: True).
follow_symlinks: resolve self if self is symlink (default: True).
Raises:
NotADirectoryError: Directory can not be made because it's a file.
Returns:
Path:
"""
path = (self / str(name)).resolve() if follow_symlinks else (self / str(name))
if not path.is_dir() and path.file_in_parents(follow_symlinks=follow_symlinks) is None:
subprocess.run(
[
*path.sudo(effective_ids=effective_ids, follow_symlinks=follow_symlinks),
f"{self.mkdir.__name__}",
"-p",
*(["-m", str(mode)] if mode else []),
path,
],
capture_output=True,
)
if passwd is not None:
path.chown(
passwd=passwd,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
)
return path
[docs]
def mv(self, dest):
"""Move.
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempdir() as tmp:
... name = 'dir'
... pth = tmp(name)
... assert pth.is_dir()
... _ = pth.mv(tmp('dir2'))
... assert not pth.is_dir()
... assert tmp('dir2').is_dir()
... name = 'file'
... pth = tmp(name, "is_file")
... assert pth.is_file()
... _ = pth.mv(tmp('file2'))
... assert not pth.is_file()
Args:
dest: destination.
Returns:
None.
"""
subprocess.run(
[*self.__class__(dest).sudo(), f"{self.mv.__name__}", self, dest],
check=True,
capture_output=True,
)
return dest
[docs]
def open( # noqa: A003
self,
mode="r",
buffering=-1,
encoding=None,
errors=None,
newline=None,
token=False,
):
"""Open the file pointed by this path and return a file object, as the built-in open function does."""
if token:
return tokenize.open(self.text) if self.is_file() else None
return super().open(
mode=mode,
buffering=buffering,
encoding=encoding,
errors=errors,
newline=newline,
)
[docs]
@classmethod
def pickle(cls, data=None, name=None, rm=False):
"""Load or dumps pickle file from ~/.pickle directory.
Examples:
>>> import pickle
>>> from nodeps import Path
>>>
>>> assert Path.pickle(name="test") is None
>>>
>>> obj = {'a': 1}
>>> _ = Path.pickle(obj, name="test")
>>> assert Path.pickle(name="test") == obj
>>>
>>> obj2 = {'a': 2}
>>> _ = Path.pickle(obj2, name="test", rm=True)
>>> assert Path.pickle(name="test") == obj2
>>>
>>> assert Path.pickle(name="test", rm=True) is None
Args:
data: data to pickle (default: None to read from file).
name: name.__name__ or name of object which will be used as file stem
(default: None to get the name from __name__ in data)
rm: rm existing data.
Raises:
InvalidArgumentError: when no name can be derived from data.__name__ or not name provided
Returns:
Pickle object (None if no data exists) if data is None else None.
"""
name = getattr(name, "__name__", None) or name or getattr(data, "__name__", None)
if name is None:
msg = f"name must be provided if {data=} does not have attribute __name__"
raise InvalidArgumentError(msg)
name = name.replace("/", "_")
if not (directory := cls("~/.pickle").expanduser()).exists():
directory.mkdir()
file = directory / f"{name}.pickle"
if rm or (file.is_file() and file.stat().st_size == 0):
file.rm()
if data is None and file.is_file():
with file.open("rb") as f:
try:
return pickle.load(f) # noqa: S301
except ModuleNotFoundError:
file.rm() # No module name if source has changed.
return None
if data is None and not file.is_file():
return None
if data is not None:
with file.open("wb") as f:
pickle.dump(data, f)
return data
return None
[docs]
def privileges(self, effective_ids=True):
"""Return privileges of file.
Args:
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: True).
Returns:
Privileges:
"""
[docs]
@classmethod
def purelib(cls):
"""Returns sysconfig purelib path."""
return cls(sysconfig.get_paths()["purelib"])
[docs]
def realpath(self, exception=False):
"""Return the canonical path of the specified filename, eliminating any symbolic links encountered in the path.
Examples:
>>> from nodeps import Path
>>>
>>> assert Path('/usr/local').realpath() == Path('/usr/local')
Args:
exception: raise exception if path does not exist (default: False).
Returns:
Path with real path.
"""
return self.__class__(os.path.realpath(self, strict=not exception))
[docs]
def relative(self, path):
"""Return relative to path if is relative to path else None.
Examples:
>>> from nodeps import Path
>>>
>>> assert Path('/usr/local').relative('/usr') == Path('local')
>>> assert Path('/usr/local').relative('/usr/local') == Path('.')
>>> assert Path('/usr/local').relative('/usr/local/bin') is None
Args:
path: path.
Returns:
Relative path or None.
"""
p = Path(path).absolute()
return self.relative_to(p) if self.absolute().is_relative_to(p) else None
[docs]
def rm(self, *args, effective_ids=True, follow_symlinks=False, missing_ok=True):
"""Delete a folder/file (even if the folder is not empty).
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempdir() as tmp:
... name = 'dir'
... pth = tmp(name)
... assert pth.is_dir()
... pth.rm()
... assert not pth.is_dir()
... name = 'file'
... pth = tmp(name, "is_file")
... assert pth.is_file()
... pth.rm()
... assert not pth.is_file()
... assert Path('/tmp/a/a/a/a')().is_dir()
Raises:
FileNotFoundError: ... No such file or directory: '/tmp/foo'
Args:
*args: parts to add to self.
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: False).
follow_symlinks: True for resolved (default: False).
missing_ok: missing_ok
"""
if not missing_ok and not self.exists():
msg = f"{self} does not exist"
raise FileNotFoundError(msg)
if (path := self.add(*args)).exists():
subprocess.run(
[
*path.sudo(
force=True,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
),
f"{self.rm.__name__}",
*(["-rf"] if self.is_dir() else []),
path.resolve() if follow_symlinks else path,
],
capture_output=True,
)
[docs]
def rm_empty(self, preserve=True):
"""Remove empty directories recursive.
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempdir() as tmp:
... first = tmp("1")
...
... _ = tmp('1/2/3/4')
... first.rm_empty()
... assert first.exists() is True
... assert Path("1").exists() is False
...
... _ = tmp('1/2/3/4')
... first.rm_empty(preserve=False)
... assert first.exists() is False
...
... _ = tmp('1/2/3/4/5/6/7.py', file="is_file")
... first.rm_empty()
... assert first.exists() is True
Args:
preserve: preserve top directory (default: True).
"""
for directory, _, _ in os.walk(self, topdown=False):
d = self.__class__(directory).absolute()
ds_store = d / ".DS_Store"
if ds_store.exists():
ds_store.rm()
if len(list(d.iterdir())) == 0 and (not preserve or (d != self.absolute() and preserve)):
self.__class__(d).rmdir()
[docs]
def setid(
self,
name=None,
uid=True,
effective_ids=True,
follow_symlinks=False,
):
"""Sets the set-user-ID-on-execution or set-group-ID-on-execution bits.
Works if interpreter binary is setuid `u+s,+x` (-rwsr-xr-x), and:
- executable script and setuid interpreter on shebang (#!/usr/bin/env setuid_interpreter).
- setuid_interpreter -m module (venv would be created as root
Works if interpreter binary is setuid `g+s,+x` (-rwxr-sr-x), and:
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempdir() as p:
... a = p.touch('a')
... _ = a.setid()
... assert a.stats().suid is True
... _ = a.setid(uid=False)
... assert a.stats().sgid is True
...
... a.rm()
...
... _ = a.touch()
... b = a.setid('b')
... assert b.stats().suid is True
... assert a.cmp(b) is True
...
... _ = b.setid('b', uid=False)
... assert b.stats().sgid is True
...
... _ = a.write_text('a')
... assert a.cmp(b) is False
... b = a.setid('b')
... assert b.stats().suid is True
... assert a.cmp(b) is True
Args:
name: name to rename if provided.
uid: True to set UID bit, False to set GID bit (default: True).
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: False).
follow_symlinks: True for resolved, False for absolute and None for relative
or doesn't exist (default: True).
Returns:
Updated Path.
"""
change = False
chmod = f'{"u" if uid else "g"}+s,+x'
mod = (stat.S_ISUID if uid else stat.S_ISGID) | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
target = self.with_name(name) if name else self
if name and (not target.exists() or not self.cmp(target)):
self.cp(target, effective_ids=effective_ids, follow_symlinks=follow_symlinks)
change = True
elif target.stats().result.st_mode & mod != mod:
change = True
if target.owner() != "root":
change = True
if change:
# First: chown, second: chmod
target.chown(passwd=Passwd.from_root(), follow_symlinks=follow_symlinks)
target.chmod(
mode=chmod,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
recursive=True,
)
return target
[docs]
@classmethod
def setid_executable_is(cls):
"""True if Set user ID execution bit is set."""
return cls(sys.executable).resolve().stat().st_mode & stat.S_ISUID == stat.S_ISUID
[docs]
@classmethod
def setid_executable(cls):
"""Sets the set-user-ID-on-execution bits for sys.executable.
Returns:
Updated Path.
"""
return cls(sys.executable).resolve().setid()
[docs]
@classmethod
def setid_executable_cp(cls, name=None, uid=True):
r"""Sets the set-user-ID-on-execution or set-group-ID-on-execution bits for sys.executable.
Examples:
>>> import shutil
>>> import subprocess
>>> from nodeps import Path, MACOS, USER
>>> def test():
... f = Path.setid_executable_cp('setid_python_test')
... assert subprocess.check_output([f, '-c', 'import os;print(os.geteuid())'], text=True) == '0\n'
... if USER != "root":
... assert subprocess.check_output([f, '-c', 'import os;print(os.getuid())'], text=True) != '0\n'
... else:
... assert subprocess.check_output([f, '-c', 'import os;print(os.getuid())'], text=True) == '0\n'
... f.rm()
... assert f.exists() is False
>>> test()
Args:
name: name to rename if provided or False to add 'r' to original name (default: False).
uid: True to set UID bit, False to set GID bit (default: True).
Returns:
Updated Path.
"""
path = cls(sys.executable)
return path.setid(name=name if name else f"r{path.name}", uid=uid)
[docs]
def stats(self, follow_symlinks=False):
"""Return result of the stat() system call on this path, like os.stat() with extra parsing for bits and root.
Examples:
>>> from nodeps import Path
>>>
>>> rv = Path().stats()
>>> assert all([rv.root, rv.sgid, rv.sticky, rv.suid]) is False
>>>
>>> with Path.tempfile() as file:
... _ = file.chmod('u+s,+x')
... assert file.stats().suid is True
... _ = file.chmod('g+s,+x')
... assert file.stats().sgid is True
Args:
follow_symlinks: If False, and the last element of the path is a symbolic link,
stat will examine the symbolic link itself instead of the file
the link points to (default: False).
Returns:
PathStat namedtuple :class:`nodeps.PathStat`:
gid: file GID
group: file group name
mode: file mode string formatted as '-rwxrwxrwx'
own: user and group string formatted as 'user:group'
passwd: instance of :class:`nodeps:Passwd` for file owner
result: result of `os.stat`
root: is owned by root
sgid: group executable and sticky bit (GID bit), members execute as the executable group (i.e.: crontab)
sticky: sticky bit (directories), new files created in this directory will be owned by the directory's owner
suid: user executable and sticky bit (UID bit), user execute and as the executable owner (i.e.: sudo)
uid: file UID
user: file owner name
"""
mapping = {
"sgid": stat.S_ISGID | stat.S_IXGRP,
"suid": stat.S_ISUID | stat.S_IXUSR,
"sticky": stat.S_ISVTX,
}
result = super().stat(follow_symlinks=follow_symlinks)
passwd = Passwd(result.st_uid)
# noinspection PyArgumentList
return PathStat(
gid=result.st_gid,
group=grp.getgrgid(result.st_gid).gr_name,
mode=stat.filemode(result.st_mode),
own=f"{passwd.user}:{passwd.group}",
passwd=passwd,
result=result,
root=result.st_uid == 0,
uid=result.st_uid,
user=passwd.user,
**{i: result.st_mode & mapping[i] == mapping[i] for i in mapping},
)
[docs]
def sudo(
self,
force=False,
to_list=True,
os_mode=os.W_OK,
effective_ids=True,
follow_symlinks=False,
):
"""Returns sudo command if path or ancestors exist and is not own by user and sudo command not installed.
Examples:
>>> from nodeps import which
>>> from nodeps import Path
>>> from nodeps import SUDO
>>>
>>> assert Path('/tmp').sudo(to_list=False, follow_symlinks=True) == ''
>>> if SUDO:
... assert "sudo" in Path('/usr/bin').sudo(to_list=False)
>>> assert Path('/usr/bin/no_dir/no_file.text').sudo(to_list=False) == SUDO
>>> assert Path('no_dir/no_file.text').sudo(to_list=False) == ''
>>> assert Path('/tmp').sudo(follow_symlinks=True) == []
>>> if SUDO:
... assert Path('/usr/bin').sudo() == [SUDO]
... else:
... assert Path('/usr/bin').sudo() == []
Args:
force: if sudo installed and user is not root, return always sudo path
to_list: return starred/list for command with no shell (default: True).
os_mode: Operating-system mode bitfield. Can be F_OK to test existence,
or the inclusive-OR of R_OK, W_OK, and X_OK (default: `os.W_OK`).
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: True).
follow_symlinks: If False, and the last element of the path is a symbolic link,
access will examine the symbolic link itself instead of the file
the link points to (default: False).
Returns:
`sudo` or "", str or list.
"""
rv = SUDO
if rv and (os.geteuid if effective_ids else os.getuid)() != 0:
path = self
while path:
if path.access(
os_mode=os_mode,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
):
if not force:
rv = ""
break
if path.exists() or str(path := (path.parent.resolve() if follow_symlinks else path.parent)) == "/":
break
return ([rv] if rv else []) if to_list else rv
[docs]
def sys(self):
"""Insert self absolute if exists to sys.path 0 if it is not in sys.path.
Examples:
>>> import sys
>>> from nodeps import Path
>>>
>>> cwd = Path.cwd()
>>> cwd.sys()
>>> assert str(cwd.absolute()) in sys.path
"""
absolute = self.absolute()
if absolute.exists() and absolute.__str__() not in sys.path:
sys.path.insert(0, absolute.__str__())
@property
def text(self):
"""Path as text.
Examples:
>>> from nodeps import Path
>>>
>>> assert Path('/usr/local').text == '/usr/local'
Returns:
Path string.
"""
return str(self)
[docs]
@classmethod
@contextlib.contextmanager
def tempcd(cls, suffix=None, prefix=None, directory=None):
"""Create temporaly directory, change to it and return it.
This has the same behavior as mkdtemp but can be used as a context manager.
Upon exiting the context, the directory and everything contained
in it are removed.
Examples:
>>> from nodeps import Path
>>>
>>> work = Path.cwd()
>>> with Path.tempcd() as tmp:
... assert tmp.exists() and tmp.is_dir()
... assert Path.cwd() == tmp.resolve()
>>> assert work == Path.cwd()
>>> assert tmp.exists() is False
Args:
suffix: If 'suffix' is not None, the directory name will end with that suffix,
otherwise there will be no suffix. For example, .../T/tmpy5tf_0suffix
prefix: If 'prefix' is not None, the directory name will begin with that prefix,
otherwise a default prefix is used.. For example, .../T/prefixtmpy5tf_0
directory: If 'directory' is not None, the directory will be created in that directory (must exist,
otherwise a default directory is used. For example, DIRECTORY/tmpy5tf_0
Returns:
Directory Path.
"""
with cls.tempdir(suffix=suffix, prefix=prefix, directory=directory) as tempdir, tempdir.cd():
try:
yield tempdir
finally:
with contextlib.suppress(FileNotFoundError):
pass
[docs]
@classmethod
@contextlib.contextmanager
def tempdir(cls, suffix=None, prefix=None, directory=None):
"""Create and return tmp directory. This has the same behavior as mkdtemp but can be used as a context manager.
Upon exiting the context, the directory and everything contained in it are removed.
Examples:
>>> from nodeps import Path
>>>
>>> work = Path.cwd()
>>> with Path.tempdir() as tmpdir:
... assert tmpdir.exists() and tmpdir.is_dir()
... assert Path.cwd() != tmpdir
... assert work == Path.cwd()
>>> assert tmpdir.exists() is False
Args:
suffix: If 'suffix' is not None, the directory name will end with that suffix,
otherwise there will be no suffix. For example, .../T/tmpy5tf_0suffix
prefix: If 'prefix' is not None, the directory name will begin with that prefix,
otherwise a default prefix is used.. For example, .../T/prefixtmpy5tf_0
directory: If 'directory' is not None, the directory will be created in that directory (must exist,
otherwise a default directory is used. For example, DIRECTORY/tmpy5tf_0
Returns:
Directory Path.
"""
with tempfile.TemporaryDirectory(suffix=suffix, prefix=prefix, dir=directory) as tmp:
try:
yield cls(tmp)
finally:
with contextlib.suppress(FileNotFoundError):
pass
[docs]
@classmethod
@contextlib.contextmanager
def tempfile(
cls,
mode="w",
buffering=-1,
encoding=None,
newline=None,
suffix=None,
prefix=None,
directory=None,
delete=True,
*,
errors=None,
):
"""Create and return a temporary file.
Examples:
>>> from nodeps import Path
>>>
>>> with Path.tempfile() as tmpfile:
... assert tmpfile.exists() and tmpfile.is_file()
>>> assert tmpfile.exists() is False
Args:
mode: the mode argument to io.open (default "w+b").
buffering: the buffer size argument to io.open (default -1).
encoding: the encoding argument to `io.open` (default None)
newline: the newline argument to `io.open` (default None)
delete: whether the file is deleted on close (default True).
suffix: prefix for filename.
prefix: prefix for filename.
directory: directory.
errors: the errors' argument to `io.open` (default None)
Returns:
An object with a file-like interface; the name of the file
is accessible as its 'name' attribute. The file will be automatically
deleted when it is closed unless the 'delete' argument is set to False.
"""
with tempfile.NamedTemporaryFile(
mode=mode,
buffering=buffering,
encoding=encoding,
newline=newline,
suffix=suffix,
prefix=prefix,
dir=directory,
delete=delete,
errors=errors,
) as tmp:
try:
yield cls(tmp.name)
finally:
with contextlib.suppress(FileNotFoundError):
pass
[docs]
def to_parent(self):
"""Return Parent if is file and exists or self.
Examples:
>>> from nodeps import Path
>>>
>>> assert Path(__file__).to_parent() == Path(__file__).parent
Returns:
Path of directory if is file or self.
"""
return self.parent if self.is_file() else self
[docs]
def touch(
self,
name="",
passwd=None,
mode=None,
effective_ids=True,
follow_symlinks=False,
):
"""Add file, touch and return post_init Path. Parent paths are created.
Examples:
>>> from nodeps import Path
>>> from nodeps import Passwd
>>>
>>> import getpass
>>> with Path.tempcd() as tmp:
... file = tmp('1/2/3/4/5/6/root.py', file="is_file", passwd=Passwd.from_root())
... assert file.is_file() is True
... assert file.parent.owner() == getpass.getuser()
... assert file.owner() == "root"
...
... new = file.parent("user.py", file="is_file")
... assert new.owner() == getpass.getuser()
...
... touch = file.parent.touch("touch.py")
... assert touch.owner() == getpass.getuser()
...
... last = (file.parent / "last.py").touch()
... assert last.owner() == getpass.getuser()
... assert last.is_file() is True
...
... file.rm()
Args:
name: name.
passwd: group/user for chown, if None ownership will not be changed (default: None).
mode: mode.
effective_ids: If True, access will use the effective uid/gid instead of
the real uid/gid (default: False).
follow_symlinks: If False, I think is useless (default: False).
Returns:
Path.
"""
path = self / str(name)
path = path.resolve() if follow_symlinks else path.absolute()
if (
not path.is_file()
and not path.is_dir()
and path.parent.file_in_parents(follow_symlinks=follow_symlinks) is None
):
if not (d := path.parent).exists():
d.mkdir(
mode=mode,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
)
subprocess.run(
[
*path.sudo(effective_ids=effective_ids, follow_symlinks=follow_symlinks),
f"{self.touch.__name__}",
path,
],
capture_output=True,
check=True,
)
path.chmod(mode=mode, effective_ids=effective_ids, follow_symlinks=follow_symlinks)
if passwd is not None:
path.chown(
passwd=passwd,
effective_ids=effective_ids,
follow_symlinks=follow_symlinks,
)
return path
[docs]
def with_suffix(self, suffix=""):
"""Sets default for suffix to "", since :class:`pathlib.Path` does not have default.
Return a new path with the file suffix changed. If the path
has no suffix, add given suffix. If the given suffix is an empty
string, remove the suffix from the path.
Examples:
>>> from nodeps import Path
>>>
>>> Path("/tmp/test.txt").with_suffix()
Path('/tmp/test')
Args:
suffix: suffix (default: '')
Returns:
Path.
"""
return super().with_suffix(suffix=suffix)
[docs]
@dataclasses.dataclass
class PathStat:
"""Helper class for :func:`nodeps.Path.stats`.
Args:
gid: file GID
group: file group name
mode: file mode string formatted as '-rwxrwxrwx'
own: user and group string formatted as 'user:group'
passwd: instance of :class:`nodeps:Passwd` for file owner
result: result of os.stat
root: is owned by root
sgid: group executable and sticky bit (GID bit), members execute as the executable group (i.e.: crontab)
sticky: sticky bit (directories), new files created in this directory will be owned by the directory's owner
suid: user executable and sticky bit (UID bit), user execute and as the executable owner (i.e.: sudo)
uid: file UID
user: file user name
"""
gid: int
group: str
mode: str
own: str
passwd: Passwd
result: os.stat_result
root: bool
sgid: bool
sticky: bool
suid: bool
uid: int
user: str
[docs]
def toiter(obj, always=False, split=" "):
"""To iter.
Examples:
>>> import pathlib
>>> from nodeps import toiter
>>>
>>> assert toiter('test1') == ['test1']
>>> assert toiter('test1 test2') == ['test1', 'test2']
>>> assert toiter({'a': 1}) == {'a': 1}
>>> assert toiter({'a': 1}, always=True) == [{'a': 1}]
>>> assert toiter('test1.test2') == ['test1.test2']
>>> assert toiter('test1.test2', split='.') == ['test1', 'test2']
>>> assert toiter(pathlib.Path("/tmp/foo")) == ('/', 'tmp', 'foo')
Args:
obj: obj.
always: return any iterable into a list.
split: split for str.
Returns:
Iterable.
"""
if isinstance(obj, str):
obj = obj.split(split)
elif hasattr(obj, "parts"):
obj = obj.parts
elif not isinstance(obj, Iterable) or always:
obj = [obj]
return obj
AnyPath: TypeAlias = Path | StrOrBytesPath | IO[AnyStr]