charmlibs-pathops 0.0.0a1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,654 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Implementation of ContainerPath class."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import errno
20
+ import pathlib
21
+ import re
22
+ import typing
23
+
24
+ import ops
25
+ from ops import pebble
26
+
27
+ from . import _constants, _errors, _fileinfo
28
+
29
+ if typing.TYPE_CHECKING:
30
+ import os
31
+ from typing import Generator, Literal
32
+
33
+ from typing_extensions import Self, TypeGuard
34
+
35
+
36
+ class RelativePathError(ValueError):
37
+ """ContainerPath only supports absolute paths.
38
+
39
+ This is because Pebble only works with absolute paths. Relative path support
40
+ will likely remain unavailable at least until Pebble supports relative paths.
41
+ In the meantime, use absolute paths.
42
+ """
43
+
44
+
45
+ class ContainerPath:
46
+ r"""Implementation of :class:`PathProtocol` for Pebble-based workload containers.
47
+
48
+ Example::
49
+
50
+ container = self.unit.get_container('c')
51
+ ContainerPath(pathlib.Path('/foo'), container=container)
52
+ ContainerPath('/', 'foo', container=container)
53
+
54
+ :class:`str` follows the :mod:`pathlib` convention and returns the string representation of
55
+ the path. :class:`ContainerPath` return the string representation of the path in the remote
56
+ filesystem. The string representation is suitable for use with system calls (on the correct
57
+ local or remote system) and Pebble layers.
58
+
59
+ Comparison methods compare by path. A :class:`ContainerPath` is only comparable to another
60
+ object if it is also a :class:`ContainerPath` on the same :class:`ops.Container`. If this is
61
+ not the case, then equality is ``False`` and other comparisons are :class:`NotImplemented`.
62
+
63
+ Args:
64
+ \*parts: components of the path, like :class:`pathlib.Path` constructor.
65
+ container: used to communicate with the workload container. Required.
66
+ Must be provided as a keyword argument.
67
+
68
+ Raises:
69
+ RelativePathError: If instantiated with a relative path.
70
+ """
71
+
72
+ def __init__(self, *parts: str | os.PathLike[str], container: ops.Container) -> None:
73
+ self._container = container
74
+ self._path = pathlib.PurePosixPath(*parts)
75
+ if not self._path.is_absolute():
76
+ raise RelativePathError(
77
+ f'ContainerPath arguments resolve to relative path: {self._path}'
78
+ )
79
+
80
+ #############################
81
+ # protocol PurePath methods #
82
+ #############################
83
+
84
+ def __hash__(self) -> int:
85
+ """Hash the tuple (container-name, path) for efficiency."""
86
+ return hash((self._container.name, self._path))
87
+
88
+ def __repr__(self) -> str:
89
+ """Return a string representation including the class, path string, and container name."""
90
+ container_repr = f'<ops.Container {self._container.name!r}>'
91
+ return f"{type(self).__name__}('{self._path}', container={container_repr})"
92
+
93
+ def __str__(self) -> str:
94
+ """Return the string representation of the path in the container.
95
+
96
+ This is equivalent to the string representation of the :class:`pathlib.PurePath` this
97
+ :class:`ContainerPath` was instantiated with.
98
+ """
99
+ return self._path.__str__()
100
+
101
+ def as_posix(self) -> str:
102
+ """Return the string representation of the path in the container."""
103
+ return self._path.__str__()
104
+
105
+ def __lt__(self, other: Self) -> bool:
106
+ if not self._can_compare(other):
107
+ return NotImplemented
108
+ return self._path < other._path
109
+
110
+ def __le__(self, other: Self) -> bool:
111
+ if not self._can_compare(other):
112
+ return NotImplemented
113
+ return self._path <= other._path
114
+
115
+ def __gt__(self, other: Self) -> bool:
116
+ if not self._can_compare(other):
117
+ return NotImplemented
118
+ return self._path > other._path
119
+
120
+ def __ge__(self, other: Self) -> bool:
121
+ if not self._can_compare(other):
122
+ return NotImplemented
123
+ return self._path >= other._path
124
+
125
+ def __eq__(self, other: object, /) -> bool:
126
+ return self._can_compare(other) and self._path == other._path
127
+
128
+ def _can_compare(self, other: object) -> TypeGuard[Self]:
129
+ return isinstance(other, ContainerPath) and other._container.name == self._container.name
130
+
131
+ def __truediv__(self, key: str | os.PathLike[str]) -> Self:
132
+ """Return a new ``ContainerPath`` with the same container and the joined path.
133
+
134
+ The joined path is equivalent to ``str(self) / pathlib.PurePath(key)``.
135
+
136
+ .. warning::
137
+ ``__rtruediv__`` is not supported, as :class:`ContainerPath` only supports absolute
138
+ paths. You likely wouldn't want to provide an absolute path as the right-hand operand,
139
+ because the absolute path would completely replace the left-hand path.
140
+ """
141
+ return self.with_segments(self._path, key)
142
+
143
+ def is_absolute(self) -> bool:
144
+ """Return whether the path is absolute (has a root), which is always the case.
145
+
146
+ Always ``True``, since initialising a :class:`ContainerPath` with a non-absolute
147
+ path will result in a :class:`RelativePathError`.
148
+ """
149
+ return self._path.is_absolute()
150
+
151
+ def match(self, path_pattern: str) -> bool:
152
+ """Return whether this path matches the given pattern.
153
+
154
+ If the pattern is relative, matching is done from the right; otherwise, the entire path is
155
+ matched. The recursive wildcard ``'**'`` is **not** supported by this method. Matching is
156
+ always case-sensitive. Only the path is matched against, the container is not considered.
157
+ """
158
+ return self._path.match(path_pattern)
159
+
160
+ def with_name(self, name: str) -> Self:
161
+ """Return a new ContainerPath, with the same container, but with the path name replaced.
162
+
163
+ The name is the last component of the path, including any suffixes.
164
+
165
+ ::
166
+
167
+ container = self.unit.get_container('c')
168
+ path = ContainerPath('/', 'foo', 'bar.txt', container=container)
169
+ repr(path.with_name('baz.bin'))
170
+ # ContainerPath('/foo/baz.bin', container=<ops.Container 'c'>)"
171
+ """
172
+ return self.with_segments(self._path.with_name(name))
173
+
174
+ def with_suffix(self, suffix: str) -> Self:
175
+ """Return a new ContainerPath with the same container and the suffix changed.
176
+
177
+ Args:
178
+ suffix: Must start with a ``'.'``, unless it is an empty string, in which case
179
+ any existing suffix will be removed entirely.
180
+
181
+ Returns:
182
+ A new instance of the same type, updated as follows. If it contains no ``'.'``,
183
+ or ends with a ``'.'``, the ``suffix`` argument is appended to its name. Otherwise,
184
+ the last ``'.'`` and any trailing content is replaced with the ``suffix`` argument.
185
+ """
186
+ return self.with_segments(self._path.with_suffix(suffix))
187
+
188
+ def joinpath(self, *other: str | os.PathLike[str]) -> Self:
189
+ r"""Return a new ContainerPath with the same container and the new args joined to its path.
190
+
191
+ Args:
192
+ other: Any number of :class:`str` or :class:`os.PathLike` objects.
193
+ If zero are provided, an effective copy of this :class:`ContainerPath` object is
194
+ returned. \*other is joined to this object's path as with :func:`os.path.join`.
195
+ This means that if any member of other is an absolute path, all the previous
196
+ components, including this object's path, are dropped entirely.
197
+
198
+ Returns:
199
+ A new :class:`ContainerPath` with the same :class:`ops.Container` object, with its path
200
+ updated with \*other as follows. For each item in other, if it is a relative path, it
201
+ is appended to the current path. If it is an absolute path, it replaces the current
202
+ path.
203
+
204
+ .. warning::
205
+ :class:`ContainerPath` is not :class:`os.PathLike`. A :class:`ContainerPath` instance
206
+ is not a valid value for ``other``, and will result in an error.
207
+ """
208
+ return self.with_segments(self._path, *other)
209
+
210
+ @property
211
+ def parents(self) -> tuple[Self, ...]:
212
+ """A sequence of this path's logical parents. Each parent is a :class:`ContainerPath`."""
213
+ return tuple(self.with_segments(p) for p in self._path.parents)
214
+
215
+ @property
216
+ def parent(self) -> Self:
217
+ """The logical parent of this path, as a :class:`ContainerPath`."""
218
+ return self.with_segments(self._path.parent)
219
+
220
+ @property
221
+ def parts(self) -> tuple[str, ...]:
222
+ """A sequence of the components in the filesystem path. The components are strings."""
223
+ return self._path.parts
224
+
225
+ @property
226
+ def name(self) -> str:
227
+ """The final path component, or an empty string if this is the root path."""
228
+ return self._path.name
229
+
230
+ @property
231
+ def suffix(self) -> str:
232
+ """The path name's last suffix (if it has any) including the leading ``'.'``.
233
+
234
+ If the path name doesn't have a suffix, the result is an empty string.
235
+ """
236
+ return self._path.suffix
237
+
238
+ @property
239
+ def suffixes(self) -> list[str]:
240
+ r"""A list of the path name's suffixes.
241
+
242
+ Each suffix includes the leading ``'.'``.
243
+
244
+ If the path name doesn't have any suffixes, the result is an empty list.
245
+ """
246
+ return self._path.suffixes
247
+
248
+ @property
249
+ def stem(self) -> str:
250
+ """The path name, minus its last suffix.
251
+
252
+ Where :meth:`name` == :meth:`stem` + :meth:`suffix`
253
+ """
254
+ return self._path.stem
255
+
256
+ #########################
257
+ # protocol Path methods #
258
+ #########################
259
+
260
+ def read_text(self, *, newline: str | None = None) -> str:
261
+ r"""Read a remote file as text and return the contents as a string.
262
+
263
+ Compared to pathlib.Path.read_text, this method drops the encoding and errors args.
264
+ The encoding is assumed to be UTF-8, and any errors encountered will be raised.
265
+
266
+ Args:
267
+ newline: if ``None`` (default), all newlines ``('\r\n', '\r', '\n')`` are replaced
268
+ with ``'\n'``. Otherwise the file contents are returned unmodified.
269
+
270
+ Returns:
271
+ The contents of the the path as a string.
272
+
273
+ Raises:
274
+ FileNotFoundError: if the parent directory does not exist.
275
+ IsADirectoryError: if the target is a directory.
276
+ PermissionError: if the Pebble user does not have permissions for the operation.
277
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
278
+ """
279
+ text = self._pull(text=True)
280
+ if newline is None:
281
+ return re.sub(r'\r\n|\r', '\n', text)
282
+ return text
283
+
284
+ def read_bytes(self) -> bytes:
285
+ """Read a remote file as bytes and return the contents.
286
+
287
+ Returns:
288
+ The contents of the the path as byes.
289
+
290
+ Raises:
291
+ FileNotFoundError: if the parent directory does not exist.
292
+ IsADirectoryError: if the target is a directory.
293
+ PermissionError: if the Pebble user does not have permissions for the operation.
294
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
295
+ """
296
+ return self._pull(text=False)
297
+
298
+ @typing.overload
299
+ def _pull(self, *, text: Literal[True]) -> str: ...
300
+ @typing.overload
301
+ def _pull(self, *, text: Literal[False] = False) -> bytes: ...
302
+ def _pull(self, *, text: bool = False):
303
+ encoding = 'utf-8' if text else None
304
+ try:
305
+ with self._container.pull(self._path, encoding=encoding) as f:
306
+ return f.read()
307
+ except pebble.PathError as e:
308
+ msg = repr(self)
309
+ _errors.raise_if_matches_file_not_found(e, msg=msg)
310
+ _errors.raise_if_matches_is_a_directory(e, msg=msg)
311
+ _errors.raise_if_matches_permission(e, msg=msg)
312
+ raise
313
+
314
+ def iterdir(self) -> typing.Generator[Self]:
315
+ """Yield :class:`ContainerPath` objects corresponding to the directory's contents.
316
+
317
+ There are no guarantees about the order of the children. The special entries
318
+ ``'.'`` and ``'..'`` are not included.
319
+
320
+ :class:`NotADirectoryError` is raised (if appropriate) when ``iterdir()`` is called.
321
+ This follows the behaviour of :meth:`pathlib.Path.iterdir` in Python 3.13+.
322
+ Previous versions deferred the error until the generator was consumed.
323
+
324
+ Raises:
325
+ FileNotFoundError: If this path does not exist.
326
+ NotADirectoryError: If this path is not a directory.
327
+ PermissionError: If the local or remote user does not have appropriate permissions.
328
+ PebbleConnectionError: If the remote container cannot be reached.
329
+ """
330
+ info = _fileinfo.from_container_path(self) # FileNotFoundError if path doesn't exist
331
+ if info.type != pebble.FileType.DIRECTORY:
332
+ _errors.raise_not_a_directory(repr(self))
333
+ file_infos = self._container.list_files(self._path)
334
+ for f in file_infos:
335
+ yield self.with_segments(f.path)
336
+
337
+ def glob(self, pattern: str | os.PathLike[str]) -> Generator[Self]:
338
+ r"""Iterate over this directory and yield all paths matching the provided pattern.
339
+
340
+ For example, ``path.glob('*.txt')``, ``path.glob('*/foo.txt')``.
341
+
342
+ .. warning::
343
+ Recursive matching using the ``'**'`` pattern is not supported.
344
+
345
+ Args:
346
+ pattern: The pattern must be relative, meaning it cannot begin with ``'/'``.
347
+ Matching is case-sensitive.
348
+
349
+ Returns:
350
+ A generator yielding :class:`ContainerPath` objects, corresponding to those of its
351
+ children which match the pattern. If this path is not a directory, there will be no
352
+ matches.
353
+
354
+ Raises:
355
+ FileNotFoundError: If this path does not exist.
356
+ NotImplementedError: If pattern is an absolute path or it uses the ``'**'`` pattern.
357
+ PermissionError: If the remote user does not have appropriate permissions.
358
+ ValueError: If the pattern is invalid.
359
+ PebbleConnectionError: If the remote container cannot be reached.
360
+ """
361
+ return self._glob(pattern)
362
+
363
+ def _glob(self, pattern: str | os.PathLike[str], skip_is_dir: bool = False) -> Generator[Self]:
364
+ pattern_path = pathlib.PurePosixPath(pattern)
365
+ if pattern_path.is_absolute():
366
+ raise NotImplementedError('Non-relative paths are unsupported.')
367
+ elif pattern_path == pathlib.PurePosixPath('.'):
368
+ raise ValueError(f'Unacceptable pettern: {pattern!r}')
369
+ *pattern_parents, pattern_itself = pattern_path.parts
370
+ if '**' in pattern_parents:
371
+ raise NotImplementedError('Recursive glob is not supported.')
372
+ if '**' in str(pattern):
373
+ raise ValueError("Invalid pattern: '**' can only be an entire path component")
374
+ if not skip_is_dir and not self.is_dir():
375
+ yield from ()
376
+ return
377
+ if not pattern_parents:
378
+ file_infos = self._container.list_files(self._path, pattern=pattern_itself)
379
+ for f in file_infos:
380
+ yield self.with_segments(f.path)
381
+ return
382
+ first, *rest = pattern_parents
383
+ next_pattern = pathlib.PurePosixPath(*rest, pattern_itself)
384
+ if first == '*':
385
+ for container_path in self.iterdir():
386
+ if container_path.is_dir():
387
+ yield from container_path._glob(next_pattern, skip_is_dir=True)
388
+ elif '*' in first:
389
+ for container_path in self._glob(first):
390
+ if container_path.is_dir():
391
+ yield from container_path._glob(next_pattern, skip_is_dir=True)
392
+ else:
393
+ yield from (self / first)._glob(next_pattern)
394
+
395
+ def owner(self) -> str:
396
+ """Return the user name of the file owner.
397
+
398
+ Raises:
399
+ FileNotFoundError: If the path does not exist.
400
+ PebbleConnectionError: If the remote container cannot be reached.
401
+ """
402
+ info = _fileinfo.from_container_path(self) # FileNotFoundError if path doesn't exist
403
+ return info.user or ''
404
+
405
+ def group(self) -> str:
406
+ """Return the group name of the file.
407
+
408
+ Raises:
409
+ FileNotFoundError: If the path does not exist.
410
+ PebbleConnectionError: If the remote container cannot be reached.
411
+ """
412
+ info = _fileinfo.from_container_path(self) # FileNotFoundError if path doesn't exist
413
+ return info.group or ''
414
+
415
+ def exists(self) -> bool:
416
+ """Whether this path exists.
417
+
418
+ Will follow symlinks to determine if the symlink target exists. This means that this
419
+ method will return ``False`` for a broken symlink.
420
+
421
+ Raises:
422
+ PebbleConnectionError: If the remote container cannot be reached.
423
+ """
424
+ return self._exists_and_matches(filetype=None)
425
+
426
+ def is_dir(self) -> bool:
427
+ """Whether this path exists and is a directory.
428
+
429
+ Will follow symlinks to determine if the symlink target exists and is a directory.
430
+
431
+ Raises:
432
+ PebbleConnectionError: If the remote container cannot be reached.
433
+ """
434
+ return self._exists_and_matches(pebble.FileType.DIRECTORY)
435
+
436
+ def is_file(self) -> bool:
437
+ """Whether this path exists and is a regular file.
438
+
439
+ Will follow symlinks to determine if the symlink target exists and is a regular file.
440
+
441
+ Raises:
442
+ PebbleConnectionError: If the remote container cannot be reached.
443
+ """
444
+ return self._exists_and_matches(pebble.FileType.FILE)
445
+
446
+ def is_fifo(self) -> bool:
447
+ """Whether this path exists and is a named pipe (also called a FIFO).
448
+
449
+ Will follow symlinks to determine if the symlink target exists and is a named pipe.
450
+
451
+ Raises:
452
+ PebbleConnectionError: If the remote container cannot be reached.
453
+ """
454
+ return self._exists_and_matches(pebble.FileType.NAMED_PIPE)
455
+
456
+ def is_socket(self) -> bool:
457
+ """Whether this path exists and is a socket.
458
+
459
+ Will follow symlinks to determine if the symlink target exists and is a socket.
460
+
461
+ Raises:
462
+ PebbleConnectionError: If the remote container cannot be reached.
463
+ """
464
+ return self._exists_and_matches(pebble.FileType.SOCKET)
465
+
466
+ def _exists_and_matches(self, filetype: pebble.FileType | None) -> bool:
467
+ info = self._try_get_fileinfo()
468
+ if info is None:
469
+ return False
470
+ if filetype is None: # we only care if the file exists
471
+ return True
472
+ return info.type is filetype
473
+
474
+ def _try_get_fileinfo(self) -> pebble.FileInfo | None:
475
+ try:
476
+ return _fileinfo.from_container_path(self)
477
+ except FileNotFoundError:
478
+ pass
479
+ except OSError as e:
480
+ if e.errno != errno.ELOOP:
481
+ raise
482
+ # else: too many levels of symbolic links
483
+ return None
484
+
485
+ ##################################################
486
+ # protocol Path methods with extended signatures #
487
+ ##################################################
488
+
489
+ def write_bytes(
490
+ self,
491
+ data: bytes | bytearray | memoryview,
492
+ *,
493
+ mode: int = _constants.DEFAULT_WRITE_MODE,
494
+ user: str | None = None,
495
+ group: str | None = None,
496
+ ) -> int:
497
+ """Write the provided data to the corresponding path in the remote container.
498
+
499
+ Compared to :meth:`pathlib.Path.write_bytes`, this method adds ``mode``, ``user``
500
+ and ``group`` args. These are forwarded to Pebble, which sets these on file creation.
501
+
502
+ Args:
503
+ data: The bytes to write. If data is a :class:`bytearray` or :class:`memoryview`, it
504
+ will be converted to :class:`bytes` in memory first.
505
+ mode: The permissions to set on the file. Defaults to 0o644 (-rw-rw-r--).
506
+ user: The name of the user to set for the file.
507
+ group: The name of the group to set for the file.
508
+
509
+ Returns: The number of bytes written.
510
+
511
+ Raises:
512
+ FileNotFoundError: if the parent directory does not exist.
513
+ LookupError: if the user or group is unknown.
514
+ NotADirectoryError: if the parent exists as a non-directory file.
515
+ PermissionError: if the Pebble user does not have permissions for the operation.
516
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
517
+ """
518
+ if isinstance(data, (bytearray, memoryview)):
519
+ # TODO: update ops to correctly test for bytearray and memoryview in push
520
+ data = bytes(data)
521
+ try:
522
+ self._container.push(
523
+ path=self._path,
524
+ source=data,
525
+ make_dirs=False,
526
+ permissions=mode,
527
+ user=user if isinstance(user, str) else None,
528
+ group=group if isinstance(group, str) else None,
529
+ )
530
+ except pebble.PathError as e:
531
+ _errors.raise_if_matches_lookup(e, msg=e.message)
532
+ msg = repr(self)
533
+ _errors.raise_if_matches_file_not_found(e, msg=msg)
534
+ _errors.raise_if_matches_not_a_directory(e, msg=msg)
535
+ _errors.raise_if_matches_permission(e, msg=msg)
536
+ raise
537
+ return len(data)
538
+
539
+ def write_text(
540
+ self,
541
+ data: str,
542
+ *,
543
+ mode: int = _constants.DEFAULT_WRITE_MODE,
544
+ user: str | None = None,
545
+ group: str | None = None,
546
+ ) -> int:
547
+ """Write the provided string to the corresponding path in the remote container.
548
+
549
+ Compared to :meth:`pathlib.Path.write_text`, this method drops the ``encoding`` and
550
+ ``errors`` args to simplify the API. The args ``mode``, ``user`` and ``group`` are added,
551
+ and are forwarded to Pebble, which sets these on file creation.
552
+
553
+ Args:
554
+ data: The string to write. Will be encoded to :class:`bytes` in memory as UTF-8,
555
+ raising any errors. Newlines are not modified on writing.
556
+ mode: The permissions to set on the file. Defaults to 0o644 (-rw-rw-r--).
557
+ user: The name of the user to set for the file.
558
+ group: The name of the group to set for the file.
559
+
560
+ Returns: The number of bytes written.
561
+
562
+ Raises:
563
+ LookupError: if the user or group is unknown.
564
+ FileNotFoundError: if the parent directory does not exist.
565
+ NotADirectoryError: if the parent exists as a non-directory file.
566
+ PermissionError: if the Pebble user does not have permissions for the operation.
567
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
568
+ """
569
+ encoded_data = data.encode()
570
+ return self.write_bytes(encoded_data, mode=mode, user=user, group=group)
571
+
572
+ def mkdir(
573
+ self,
574
+ mode: int = _constants.DEFAULT_MKDIR_MODE,
575
+ parents: bool = False,
576
+ exist_ok: bool = False,
577
+ *,
578
+ user: str | None = None,
579
+ group: str | None = None,
580
+ ) -> None:
581
+ """Create a new directory at the corresponding path in the remote container.
582
+
583
+ Compared to :meth:`pathlib.Path.mkdir`, this method adds ``user`` and ``group`` args.
584
+ These are used to set the ownership of the created directory. Any created parents
585
+ will not have their ownership set.
586
+
587
+ Args:
588
+ mode: The permissions to set on the created directory. Any parents created will have
589
+ their permissions set to the default value of 0o755 (drwxr-xr-x).
590
+ parents: Whether to create any missing parent directories as well. If ``False``
591
+ (default) and a parent directory does not exist, a :class:`FileNotFound` error will
592
+ be raised.
593
+ exist_ok: Whether to raise an error if the directory already exists.
594
+ If ``False`` (default) and the directory already exists,
595
+ a :class:`FileExistsError` will be raised.
596
+ user: The name of the user to set for the directory.
597
+ group: The name of the group to set for the directory.
598
+
599
+ Raises:
600
+ FileExistsError: if the directory already exists and ``exist_ok`` is ``False``.
601
+ FileNotFoundError: if the parent directory does not exist and ``parents`` is ``False``.
602
+ LookupError: if the user or group is unknown.
603
+ NotADirectoryError: if the parent exists as a non-directory file.
604
+ PermissionError: if the remote user does not have permissions for the operation.
605
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
606
+ """
607
+ if parents and not exist_ok and self.exists():
608
+ raise _errors.raise_file_exists(repr(self))
609
+ elif not parents and exist_ok and not self.parent.exists():
610
+ _errors.raise_file_not_found(repr(self.parent))
611
+ if parents:
612
+ # create parents with default permissions, following pathlib
613
+ self._container.make_dir(
614
+ path=self._path.parent,
615
+ make_parents=True,
616
+ permissions=_constants.DEFAULT_MKDIR_MODE,
617
+ )
618
+ try:
619
+ self._container.make_dir(
620
+ path=self._path,
621
+ make_parents=exist_ok, # parents created separately above
622
+ permissions=mode,
623
+ user=user if isinstance(user, str) else None,
624
+ group=group if isinstance(group, str) else None,
625
+ )
626
+ except pebble.PathError as e:
627
+ _errors.raise_if_matches_lookup(e, msg=e.message)
628
+ msg = repr(self)
629
+ if _errors.matches_not_a_directory(e):
630
+ # target exists and isn't a directory, or parent isn't a directory
631
+ if not self.parent.is_dir():
632
+ _errors.raise_not_a_directory(msg=msg, from_=e)
633
+ _errors.raise_file_exists(repr(self), from_=e)
634
+ _errors.raise_if_matches_file_exists(e, msg=msg)
635
+ _errors.raise_if_matches_file_not_found(e, msg=msg)
636
+ _errors.raise_if_matches_permission(e, msg=msg)
637
+ raise
638
+
639
+ #############################
640
+ # non-protocol Path methods #
641
+ #############################
642
+
643
+ def with_segments(self, *pathsegments: str | os.PathLike[str]) -> Self:
644
+ """Construct a new ``ContainerPath`` (with the same container) from path-like objects.
645
+
646
+ You can think of this like a copy of the current :class:`ContainerPath`, with its path
647
+ replaced by ``pathlib.Path(*pathsegments)``.
648
+
649
+ This method is used internally by all :class:`ContainerPath` methods that return new
650
+ :class:`ContainerPath` instances, including :meth:`parent` and :meth:`parents`. Therefore,
651
+ subclasses can customise the behaviour of all such methods by overriding only this method.
652
+ The same is true of :class:`pathlib.Path` in Python 3.12+.
653
+ """
654
+ return type(self)(*pathsegments, container=self._container)