Unify docstring formats

This commit is contained in:
Kivi Kaitaniemi
2025-12-12 23:17:18 +02:00
parent 2b1bbdb884
commit 6047cad690
3 changed files with 116 additions and 118 deletions
+12 -2
View File
@@ -4,6 +4,16 @@ import typing
import decman.core.command as command import decman.core.command as command
import decman.core.output as output import decman.core.output as output
# Re-export File and Directory
from decman.core.fs import Directory, File # noqa: F401
__all__ = [
"File",
"Directory",
"prg",
"sh",
]
def prg( def prg(
cmd: list[str], cmd: list[str],
@@ -16,7 +26,7 @@ def prg(
""" """
Shortcut for running a command. Returns the output of that command. Shortcut for running a command. Returns the output of that command.
Args: Arguments:
cmd: cmd:
Command to execute. Command to execute.
@@ -74,7 +84,7 @@ def sh(
""" """
Shortcut for running a shell command. Returns the output of that command. Shortcut for running a shell command. Returns the output of that command.
Args: Arguments:
sh_cmd: sh_cmd:
Shell command to execute. The command is passed to the system shell /bin/sh. Shell command to execute. The command is passed to the system shell /bin/sh.
@@ -20,45 +20,42 @@ class File:
Ownership, permissions, and parent directories are enforced on creation. Missing parent Ownership, permissions, and parent directories are enforced on creation. Missing parent
directories are created recursively and assigned the same ownership as the file when specified. directories are created recursively and assigned the same ownership as the file when specified.
Parameters Parameters:
---------- source_file:
source_file: Path to an existing file to copy from. Mutually exclusive with ``content``.
Path to an existing file to copy from. Mutually exclusive with ``content``.
content: content:
In-memory file contents to write. Mutually exclusive with ``source_file``. In-memory file contents to write. Mutually exclusive with ``source_file``.
bin_file: bin_file:
If ``True``, treat the file as binary. Disables variable substitution and writes bytes If ``True``, treat the file as binary. Disables variable substitution and writes bytes
verbatim. verbatim.
encoding: encoding:
Text encoding used when reading or writing non-binary files. Text encoding used when reading or writing non-binary files.
owner: owner:
System user name to own the file and created parent directories. System user name to own the file and created parent directories.
group: group:
System group name to own the file and created parent directories. System group name to own the file and created parent directories.
permissions: permissions:
File mode applied to the target file (e.g. ``0o644``). File mode applied to the target file (e.g. ``0o644``).
Raises Raises:
------ ValueError
ValueError If both ``source_file`` and ``content`` are ``None`` or if both are set.
If both ``source_file`` and ``content`` are ``None`` or if both are set.
UserNotFoundError UserNotFoundError
If ``owner`` does not exist on the system. If ``owner`` does not exist on the system.
GroupNotFoundError GroupNotFoundError
If ``group`` does not exist on the system. If ``group`` does not exist on the system.
Notes Notes:
----- Variable substitution is a simple string replacement where each key in ``variables`` is
Variable substitution is a simple string replacement where each key in ``variables`` is replaced by its corresponding value. No escaping or templating semantics are applied.
replaced by its corresponding value. No escaping or templating semantics are applied.
""" """
def __init__( def __init__(
@@ -98,35 +95,31 @@ class File:
""" """
Copies the contents of this file to the target file if they differ. Copies the contents of this file to the target file if they differ.
Parameters Parameters:
---------- target:
target: Path to the target file on disk.
Path to the target file on disk.
variables: variables:
Optional mapping of literal substrings to replace in the text content before writing. Optional mapping of literal substrings to replace in the text content before
Ignored for binary files and when ``bin_file`` is True. writing. Ignored for binary files and when ``bin_file`` is True.
Returns Returns:
-------
bool
True if the file contents were created or modified. True if the file contents were created or modified.
False if the existing file already contained the desired contents. False if the existing file already contained the desired contents.
Raises Raises:
------ OSError
OSError If directory creation, file I/O, permission changes, or ownership changes fail
If directory creation, file I/O, permission changes, or ownership changes fail (e.g. permission denied, missing parent path components, I/O errors).
(e.g. permission denied, missing parent path components, I/O errors).
FileNotFoundError FileNotFoundError
If ``source_file`` is set and does not exist. If ``source_file`` is set and does not exist.
UnicodeDecodeError UnicodeDecodeError
If a text file cannot be decoded using ``encoding``. If a text file cannot be decoded using ``encoding``.
UnicodeEncodeError UnicodeEncodeError
If text content cannot be encoded using ``encoding``. If text content cannot be encoded using ``encoding``.
""" """
if variables is None: if variables is None:
variables = {} variables = {}
@@ -220,34 +213,32 @@ class Directory:
permissions, encoding, and binary/text behavior. Text files can optionally undergo permissions, encoding, and binary/text behavior. Text files can optionally undergo
variable substitution before being written. variable substitution before being written.
Parameters Parameters:
---------- source_directory:
source_directory: Path to the directory whose contents will be mirrored into the target.
Path to the directory whose contents will be mirrored into the target.
bin_files: bin_files:
If ``True``, treat all files as binary; disables variable substitution and copies bytes If ``True``, treat all files as binary; disables variable substitution and copies bytes
verbatim. verbatim.
encoding: encoding:
Text encoding used when reading or writing non-binary files. Text encoding used when reading or writing non-binary files.
owner: owner:
System user name to own created files and directories. System user name to own created files and directories.
group: group:
System group name to own created files and directories. System group name to own created files and directories.
permissions: permissions:
File mode applied to created or updated files (e.g. ``0o644``). File mode applied to created or updated files (e.g. ``0o644``).
Raises Raises:
------ UserNotFoundError
UserNotFoundError If ``owner`` does not exist on the system.
If ``owner`` does not exist on the system.
GroupNotFoundError GroupNotFoundError
If ``group`` does not exist on the system. If ``group`` does not exist on the system.
""" """
def __init__( def __init__(
@@ -287,42 +278,39 @@ class Directory:
""" """
Copies the files in this directory to the target directory. Only replaces files that differ. Copies the files in this directory to the target directory. Only replaces files that differ.
Parameters Parameters:
---------- target_directory:
target_directory: Destination directory root. Relative layout from the source is preserved beneath
Destination directory root. Relative layout from the source is preserved beneath this this path.
path.
variables: variables:
Optional mapping of literal substrings to replace in text files before writing. Ignored Optional mapping of literal substrings to replace in text files before writing.
for binary files. Ignored for binary files.
dry_run: dry_run:
If ``True``, perform a dry-run: no files are written, but the list of files that *would* If ``True``, perform a dry-run: no files are written, but the list of files that
be processed is returned. *would* be processed is returned.
Returns Returns:
------- list[str]
list[str] When ``dry_run`` is ``False``, paths of files that were created or whose contents
When ``dry_run`` is ``False``, paths of files that were created or whose contents were modified.
were modified.
When ``dry_run`` is ``True``, paths of all files that would be considered for When ``dry_run`` is ``True``, paths of all files that would be considered for
creation or modification (no changes are actually performed). creation or modification (no changes are actually performed).
Raises Raises:
------ OSError
OSError If directory traversal or file I/O fails (e.g. permission denied).
If directory traversal or file I/O fails (e.g. permission denied).
FileNotFoundError FileNotFoundError
If ``source_directory`` does not exist or becomes unavailable. If ``source_directory`` does not exist or becomes unavailable.
UnicodeDecodeError UnicodeDecodeError
If a text file cannot be decoded using ``encoding``. If a text file cannot be decoded using ``encoding``.
UnicodeEncodeError UnicodeEncodeError
If text content cannot be encoded using ``encoding``. If text content cannot be encoded using ``encoding``.
""" """
changed_or_created = [] changed_or_created = []
original_wd = os.getcwd() original_wd = os.getcwd()
+17 -17
View File
@@ -3,15 +3,15 @@ import stat
from pathlib import Path from pathlib import Path
# Adjust this import to match your actual module location # Adjust this import to match your actual module location
import decman.core.files as files import decman.core.fs as fs
# --- files.File tests -------------------------------------------------------------- # --- fs.File tests --------------------------------------------------------------
def test_file_from_content_creates_and_is_idempotent(tmp_path: Path) -> None: def test_file_from_content_creates_and_is_idempotent(tmp_path: Path) -> None:
target = tmp_path / "file.txt" target = tmp_path / "file.txt"
f = files.File(content="hello", permissions=0o600) f = fs.File(content="hello", permissions=0o600)
# First run: file must be created and reported as changed # First run: file must be created and reported as changed
changed1 = f.copy_to(str(target)) changed1 = f.copy_to(str(target))
@@ -31,7 +31,7 @@ def test_file_from_content_creates_and_is_idempotent(tmp_path: Path) -> None:
def test_file_content_with_variables_and_change_detection(tmp_path: Path) -> None: def test_file_content_with_variables_and_change_detection(tmp_path: Path) -> None:
target = tmp_path / "templated.txt" target = tmp_path / "templated.txt"
f = files.File(content="hello {{NAME}}") f = fs.File(content="hello {{NAME}}")
# First run: NAME=world # First run: NAME=world
changed1 = f.copy_to(str(target), {"{{NAME}}": "world"}) changed1 = f.copy_to(str(target), {"{{NAME}}": "world"})
@@ -55,7 +55,7 @@ def test_file_from_source_text_with_and_without_variables(tmp_path: Path) -> Non
target = tmp_path / "dst.txt" target = tmp_path / "dst.txt"
# Without variables (raw copy) # Without variables (raw copy)
f_raw = files.File(source_file=str(src)) f_raw = fs.File(source_file=str(src))
changed1 = f_raw.copy_to(str(target), {}) changed1 = f_raw.copy_to(str(target), {})
assert changed1 is True assert changed1 is True
assert target.read_text(encoding="utf-8") == "VALUE={{X}}" assert target.read_text(encoding="utf-8") == "VALUE={{X}}"
@@ -65,7 +65,7 @@ def test_file_from_source_text_with_and_without_variables(tmp_path: Path) -> Non
assert changed2 is False assert changed2 is False
# With variables (substitution) # With variables (substitution)
f_sub = files.File(source_file=str(src)) f_sub = fs.File(source_file=str(src))
changed3 = f_sub.copy_to(str(target), {"{{X}}": "42"}) changed3 = f_sub.copy_to(str(target), {"{{X}}": "42"})
assert changed3 is True assert changed3 is True
assert target.read_text(encoding="utf-8") == "VALUE=42" assert target.read_text(encoding="utf-8") == "VALUE=42"
@@ -79,7 +79,7 @@ def test_file_binary_from_content(tmp_path: Path) -> None:
target = tmp_path / "bin.dat" target = tmp_path / "bin.dat"
payload = b"\x00\x01\x02hello" payload = b"\x00\x01\x02hello"
f = files.File(content=payload.decode("latin1"), bin_file=True) f = fs.File(content=payload.decode("latin1"), bin_file=True)
changed1 = f.copy_to(str(target)) changed1 = f.copy_to(str(target))
assert changed1 is True assert changed1 is True
@@ -97,7 +97,7 @@ def test_file_binary_copy_from_source(tmp_path: Path) -> None:
src.write_bytes(payload) src.write_bytes(payload)
target = tmp_path / "dst.bin" target = tmp_path / "dst.bin"
f = files.File(source_file=str(src), bin_file=True) f = fs.File(source_file=str(src), bin_file=True)
changed1 = f.copy_to(str(target), {"IGNORED": "x"}) changed1 = f.copy_to(str(target), {"IGNORED": "x"})
assert changed1 is True assert changed1 is True
@@ -113,7 +113,7 @@ def test_file_creates_parent_directories_and_applies_permissions(tmp_path: Path)
nested_dir = tmp_path / "a" / "b" / "c" nested_dir = tmp_path / "a" / "b" / "c"
target = nested_dir / "file.txt" target = nested_dir / "file.txt"
f = files.File(content="data", permissions=0o644) f = fs.File(content="data", permissions=0o644)
changed = f.copy_to(str(target)) changed = f.copy_to(str(target))
assert changed is True assert changed is True
@@ -127,7 +127,7 @@ def test_file_creates_parent_directories_and_applies_permissions(tmp_path: Path)
assert mode == 0o644 assert mode == 0o644
# --- files.Directory tests --------------------------------------------------------- # --- fs.Directory tests ---------------------------------------------------------
def _create_sample_source_tree(root: Path) -> None: def _create_sample_source_tree(root: Path) -> None:
@@ -143,14 +143,14 @@ def test_directory_copy_to_creates_and_is_idempotent(tmp_path: Path) -> None:
_create_sample_source_tree(src_dir) _create_sample_source_tree(src_dir)
d = files.Directory( d = fs.Directory(
source_directory=str(src_dir), source_directory=str(src_dir),
bin_files=False, bin_files=False,
encoding="utf-8", encoding="utf-8",
permissions=0o644, permissions=0o644,
) )
# First run: both files should be created and reported as changed # First run: both fs should be created and reported as changed
changed1 = d.copy_to(str(dst_dir), variables={"{{X}}": "1"}) changed1 = d.copy_to(str(dst_dir), variables={"{{X}}": "1"})
expected_paths = { expected_paths = {
str(dst_dir / "a.txt"), str(dst_dir / "a.txt"),
@@ -161,7 +161,7 @@ def test_directory_copy_to_creates_and_is_idempotent(tmp_path: Path) -> None:
assert (dst_dir / "a.txt").read_text(encoding="utf-8") == "A=1" assert (dst_dir / "a.txt").read_text(encoding="utf-8") == "A=1"
assert (dst_dir / "sub" / "b.txt").read_text(encoding="utf-8") == "B=1" assert (dst_dir / "sub" / "b.txt").read_text(encoding="utf-8") == "B=1"
# Second run with same variables: no files should be reported as changed # Second run with same variables: no fs should be reported as changed
changed2 = d.copy_to(str(dst_dir), variables={"{{X}}": "1"}) changed2 = d.copy_to(str(dst_dir), variables={"{{X}}": "1"})
assert changed2 == [] assert changed2 == []
@@ -172,7 +172,7 @@ def test_directory_copy_to_detects_changes_via_variables(tmp_path: Path) -> None
src_dir.mkdir() src_dir.mkdir()
_create_sample_source_tree(src_dir) _create_sample_source_tree(src_dir)
d = files.Directory(source_directory=str(src_dir)) d = fs.Directory(source_directory=str(src_dir))
# Initial materialization # Initial materialization
changed1 = d.copy_to(str(dst_dir), variables={"{{X}}": "alpha"}) changed1 = d.copy_to(str(dst_dir), variables={"{{X}}": "alpha"})
@@ -181,7 +181,7 @@ def test_directory_copy_to_detects_changes_via_variables(tmp_path: Path) -> None
str(dst_dir / "sub" / "b.txt"), str(dst_dir / "sub" / "b.txt"),
} }
# Change variables -> both files change # Change variables -> both fs change
changed2 = d.copy_to(str(dst_dir), variables={"{{X}}": "beta"}) changed2 = d.copy_to(str(dst_dir), variables={"{{X}}": "beta"})
assert set(changed2) == { assert set(changed2) == {
str(dst_dir / "a.txt"), str(dst_dir / "a.txt"),
@@ -198,7 +198,7 @@ def test_directory_copy_to_dry_run(tmp_path: Path) -> None:
src_dir.mkdir() src_dir.mkdir()
_create_sample_source_tree(src_dir) _create_sample_source_tree(src_dir)
d = files.Directory(source_directory=str(src_dir)) d = fs.Directory(source_directory=str(src_dir))
# First, actually materialize once # First, actually materialize once
d.copy_to(str(dst_dir), variables={"{{X}}": "1"}) d.copy_to(str(dst_dir), variables={"{{X}}": "1"})
@@ -230,7 +230,7 @@ def test_directory_copy_to_restores_working_directory(tmp_path: Path) -> None:
src_dir.mkdir() src_dir.mkdir()
_create_sample_source_tree(src_dir) _create_sample_source_tree(src_dir)
d = files.Directory(source_directory=str(src_dir)) d = fs.Directory(source_directory=str(src_dir))
original_cwd = os.getcwd() original_cwd = os.getcwd()
try: try: