charmlibs-pathops 0.0.0__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,721 @@
1
+ # Copyright 2024 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
+ """Type-checking exclusive definitions."""
16
+
17
+ from __future__ import annotations
18
+
19
+ import typing
20
+
21
+ from . import _constants
22
+
23
+ if typing.TYPE_CHECKING:
24
+ import os
25
+ from typing import Generator, Sequence
26
+
27
+ from typing_extensions import Self
28
+
29
+
30
+ # based on typeshed.stdlib.pathlib.PurePath
31
+ # https://github.com/python/typeshed/blob/main/stdlib/pathlib.pyi#L29
32
+ class PathProtocol(typing.Protocol):
33
+ """The protocol implemented by :class:`ContainerPath` and :class:`LocalPath`.
34
+
35
+ Use this class in type annotations where either :class:`ContainerPath` or
36
+ :class:`LocalPath` is acceptable. This will result in both correct type checking
37
+ and useful autocompletions.
38
+
39
+ While using a union type will also give correct type checking results, it provides less
40
+ useful autocompletions, as most editors will autocomplete methods and attributes that *any*
41
+ of the union members have, rather than only those that *all* of the union members have.
42
+
43
+ :class:`str` follows the :mod:`pathlib` convention and returns the string representation of
44
+ the path. :class:`ContainerPath` return the string representation of the path in the remote
45
+ filesystem. The string representation is suitable for use with system calls (on the correct
46
+ local or remote system) and Pebble layers.
47
+
48
+ Comparison methods compare by path. A :class:`ContainerPath` is only comparable to another
49
+ object if it is also a :class:`ContainerPath` on the same :class:`ops.Container`. If this is
50
+ not the case, then equality is ``False`` and other comparisons are :class:`NotImplemented`.
51
+
52
+ Protocol implementers are hashable.
53
+ """
54
+
55
+ #############################
56
+ # protocol PurePath methods #
57
+ #############################
58
+
59
+ def __hash__(self) -> int: ...
60
+
61
+ # comparison methods
62
+ def __lt__(self, other: Self) -> bool: ...
63
+
64
+ def __le__(self, other: Self) -> bool: ...
65
+
66
+ def __gt__(self, other: Self) -> bool: ...
67
+
68
+ def __ge__(self, other: Self) -> bool: ...
69
+
70
+ def __eq__(self, other: object, /) -> bool: ...
71
+
72
+ def __truediv__(self, key: str | os.PathLike[str]) -> Self:
73
+ """Return a new instance of the same type, with the other operand appended to its path.
74
+
75
+ Used as ``protocol_implementor / str_or_pathlike``.
76
+
77
+ .. note::
78
+ ``__rtruediv__`` is currently not part of this protocol, as
79
+ :class:`ContainerPath` objects only support absolute paths.
80
+ """
81
+ ...
82
+
83
+ def __str__(self) -> str: ...
84
+
85
+ def as_posix(self) -> str:
86
+ """Return the string representation of the path.
87
+
88
+ Exactly like :class:`pathlib.Path` for local paths. Following this convention,
89
+ remote filesystem paths such as :class:`ContainerPath` return the string representation
90
+ of the path in the remote filesystem. The string representation is suitable for
91
+ use with system calls (on the correct local or remote system) and Pebble layers.
92
+
93
+ Identical to :class:`str`, since we only support posix systems.
94
+ """
95
+ ...
96
+
97
+ def is_absolute(self) -> bool:
98
+ """Return whether the path is absolute (has a root).
99
+
100
+ .. note::
101
+ Currently always ``True`` for :class:`ContainerPath`.
102
+ """
103
+ ...
104
+
105
+ # NOTE: Not supported -- (Python 3.12) ``case_sensitive`` keyword argument
106
+ def match(self, path_pattern: str) -> bool:
107
+ """Return whether this path matches the given pattern.
108
+
109
+ If the pattern is relative, matching is done from the right; otherwise, the entire path is
110
+ matched. The recursive wildcard ``'**'`` is **not** supported by this method. Matching is
111
+ always case-sensitive.
112
+ """
113
+ ...
114
+
115
+ def with_name(self, name: str) -> Self:
116
+ """Return a new instance of the same type, with the path name replaced.
117
+
118
+ The name is the last component of the path, including any suffixes.
119
+ """
120
+ ...
121
+
122
+ def with_suffix(self, suffix: str) -> Self:
123
+ """Return a new instance of the same type, with the path suffix replaced.
124
+
125
+ Args:
126
+ suffix: Must start with a ``'.'``, unless it is an empty string, in which case
127
+ any existing suffix will be removed entirely.
128
+
129
+ Returns:
130
+ A new instance of the same type, updated as follows. If it contains no ``'.'``,
131
+ or ends with a ``'.'``, the ``suffix`` argument is appended to its name. Otherwise,
132
+ the last ``'.'`` and any trailing content is replaced with the ``suffix`` argument.
133
+ """
134
+ ...
135
+
136
+ def joinpath(self, *other: str | os.PathLike[str]) -> Self:
137
+ """Return a new instance of the same type, with its path joined with the arguments.
138
+
139
+ Args:
140
+ other: Any number of :class:`str` or :class:`os.PathLike` objects.
141
+
142
+ Returns:
143
+ A new instance of the same type, updated as follows. For each item in other,
144
+ if it is a relative path, it is appended to the current path. If it is an absolute
145
+ path, it replaces the current path.
146
+
147
+ .. warning::
148
+ :class:`ContainerPath` is not :class:`os.PathLike`. A :class:`ContainerPath` instance
149
+ is not a valid value for ``other``, and will result in an error.
150
+ """
151
+ ...
152
+
153
+ @property
154
+ def parents(self) -> Sequence[Self]:
155
+ """A sequence of this path's logical parents. Each parent is an instance of this type."""
156
+ ...
157
+
158
+ @property
159
+ def parent(self) -> Self:
160
+ """The logical parent of this path. An instance of this type."""
161
+ ...
162
+
163
+ @property
164
+ def parts(self) -> tuple[str, ...]:
165
+ """A sequence of the components in the filesystem path."""
166
+ ...
167
+
168
+ @property
169
+ def name(self) -> str:
170
+ """The final path component, or an empty string if this is the root path."""
171
+ ...
172
+
173
+ @property
174
+ def suffix(self) -> str:
175
+ """The path name's last suffix (if it has any) including the leading ``'.'``.
176
+
177
+ If the path name doesn't have a suffix, the result is an empty string.
178
+ """
179
+ ...
180
+
181
+ @property
182
+ def suffixes(self) -> list[str]:
183
+ r"""A list of the path name's suffixes.
184
+
185
+ Each suffix includes the leading ``'.'``.
186
+
187
+ If the path name doesn't have any suffixes, the result is an empty list.
188
+ """
189
+ ...
190
+
191
+ @property
192
+ def stem(self) -> str:
193
+ """The path name, minus its last suffix.
194
+
195
+ Where :meth:`name` == :meth:`stem` + :meth:`suffix`
196
+ """
197
+ ...
198
+
199
+ #########################
200
+ # protocol Path methods #
201
+ #########################
202
+
203
+ # NOTE: Not supported -- (Python 3.13) ``newline`` argument
204
+ def read_text(self) -> str:
205
+ """Read the corresponding local or remote filesystem path and return the string contents.
206
+
207
+ Compared to :meth:`pathlib.Path.read_text`, this method drops the ``encoding`` and
208
+ ``errors`` args to simplify the API.
209
+
210
+ Returns:
211
+ The UTF-8 decoded contents of the file as a :class:`str`.
212
+
213
+ Raises:
214
+ FileNotFoundError: If this path does not exist.
215
+ PermissionError: If the local or remote user does not have read permissions.
216
+ UnicodeError: If the file's contents are not valid UTF-8.
217
+ PebbleConnectionError: If the remote container cannot be reached.
218
+ """
219
+ ...
220
+
221
+ def read_bytes(self) -> bytes:
222
+ """Read the corresponding local or remote filesystem path and return the binary contents.
223
+
224
+ Returns:
225
+ The file's raw contents as :class:`bytes`.
226
+
227
+ Raises:
228
+ FileNotFoundError: If this path does not exist.
229
+ PermissionError: If the local or remote user does not have read permissions.
230
+ PebbleConnectionError: If the remote container cannot be reached.
231
+ """
232
+ ...
233
+
234
+ def iterdir(self) -> Generator[Self]:
235
+ """Yield objects of the same type corresponding to the directory's contents.
236
+
237
+ There are no guarantees about the order of the children. The special entries
238
+ ``'.'`` and ``'..'`` are not included.
239
+
240
+ Raises:
241
+ FileNotFoundError: If this path does not exist.
242
+ NotADirectoryError: If this path is not a directory.
243
+ PermissionError: If the local or remote user does not have appropriate permissions.
244
+ PebbleConnectionError: If the remote container cannot be reached.
245
+ """
246
+ ...
247
+
248
+ # NOTE: Not supported -- (Python 3.12) ``case_sensitive`` argument
249
+ # NOTE: Not supported -- (Python 3.13) ``pattern`` can be path-like
250
+ # NOTE: Not supported -- (Python 3.13) ``recurse_symlinks``
251
+ def glob(self, pattern: str) -> Generator[Self]:
252
+ r"""Iterate over this directory and yield all paths matching the provided pattern.
253
+
254
+ For example, ``path.glob('*.txt')``, ``path.glob('*/foo.txt')``.
255
+
256
+ .. warning::
257
+ Recursive matching, using the ``'**'`` pattern, is not supported by
258
+ :meth:`ContainerPath.glob`.
259
+
260
+ Args:
261
+ pattern: Must be relative, meaning it cannot begin with ``'/'``.
262
+ Matching is case-sensitive.
263
+
264
+ Returns:
265
+ A generator yielding objects of the same type as this path, corresponding to those
266
+ of its children which match the pattern. If this path is not a directory, there will
267
+ be no matches.
268
+
269
+ Raises:
270
+ FileNotFoundError: If this path does not exist.
271
+ NotImplementedError: If pattern is an absolute path, or (in the case of
272
+ :class:`ContainerPath`) if it uses the ``'**'`` pattern.
273
+ PermissionError: If the local or remote user does not have appropriate permissions.
274
+ ValueError: If the pattern is invalid.
275
+ PebbleConnectionError: If the remote container cannot be reached.
276
+ """
277
+ ...
278
+
279
+ def owner(self) -> str:
280
+ """Return the user name of the file owner.
281
+
282
+ Raises:
283
+ FileNotFoundError: If the path does not exist.
284
+ PebbleConnectionError: If the remote container cannot be reached.
285
+ """
286
+ ...
287
+
288
+ def group(self) -> str:
289
+ """Return the group name of the file.
290
+
291
+ Raises:
292
+ FileNotFoundError: If the path does not exist.
293
+ PebbleConnectionError: If the remote container cannot be reached.
294
+ """
295
+ ...
296
+
297
+ # NOTE: Not supported -- (Python 3.12) ``follow_symlinks`` argument (default=True)
298
+ def exists(self) -> bool:
299
+ """Whether this path exists.
300
+
301
+ Will follow symlinks to determine if the symlink target exists. This means that this
302
+ method will return ``False`` for a broken symlink.
303
+
304
+ Raises:
305
+ PebbleConnectionError: If the remote container cannot be reached.
306
+ """
307
+ ...
308
+
309
+ # NOTE: Not supported -- (Python 3.13) ``follow_symlinks`` argument (default=True)
310
+ def is_dir(self) -> bool:
311
+ """Whether this path exists and is a directory.
312
+
313
+ Will follow symlinks to determine if the symlink target exists and is a directory.
314
+
315
+ Raises:
316
+ PebbleConnectionError: If the remote container cannot be reached.
317
+ """
318
+ ...
319
+
320
+ # NOTE: Not supported -- (Python 3.13) ``follow_symlinks`` argument (default=True)
321
+ def is_file(self) -> bool:
322
+ """Whether this path exists and is a regular file.
323
+
324
+ Will follow symlinks to determine if the symlink target exists and is a regular file.
325
+
326
+ Raises:
327
+ PebbleConnectionError: If the remote container cannot be reached.
328
+ """
329
+ ...
330
+
331
+ def is_fifo(self) -> bool:
332
+ """Whether this path exists and is a named pipe (also called a FIFO).
333
+
334
+ Will follow symlinks to determine if the symlink target exists and is a named pipe.
335
+
336
+ Raises:
337
+ PebbleConnectionError: If the remote container cannot be reached.
338
+ """
339
+ ...
340
+
341
+ def is_socket(self) -> bool:
342
+ """Whether this path exists and is a socket.
343
+
344
+ Will follow symlinks to determine if the symlink target exists and is a socket.
345
+
346
+ Raises:
347
+ PebbleConnectionError: If the remote container cannot be reached.
348
+ """
349
+ ...
350
+
351
+ def is_symlink(self) -> bool:
352
+ """Whether this path is a symbolic link.
353
+
354
+ Raises:
355
+ PebbleConnectionError: If the remote container cannot be reached.
356
+ """
357
+ ...
358
+
359
+ def rmdir(self) -> None:
360
+ """Remove this path if it is an empty directory.
361
+
362
+ Raises:
363
+ FileNotFoundError: if the path does not exist.
364
+ NotADirectoryError: if the path exists but is not a directory.
365
+ PermissionError: if the local or remote user does not have the appropriate permissions.
366
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
367
+ """
368
+ ...
369
+
370
+ def unlink(self) -> None:
371
+ """Remove this path if it is an empty directory.
372
+
373
+ Raises:
374
+ FileNotFoundError: if the path does not exist.
375
+ IsADirectoryError: if the path exists but is a directory.
376
+ PermissionError: if the local or remote user does not have the appropriate permissions.
377
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
378
+ """
379
+ ...
380
+
381
+ ##################################################
382
+ # protocol Path methods with extended signatures #
383
+ ##################################################
384
+
385
+ def write_bytes(
386
+ self,
387
+ data: bytes,
388
+ *,
389
+ mode: int | None = None,
390
+ user: str | None = None,
391
+ group: str | None = None,
392
+ ) -> int:
393
+ """Write the provided data to the corresponding path.
394
+
395
+ Compared to :meth:`pathlib.Path.write_bytes`, this method adds ``mode``, ``user``
396
+ and ``group`` args, which are set on file creation.
397
+
398
+ Args:
399
+ data: The bytes to write, typically a :class:`bytes` object, but may also be a
400
+ :class:`bytearray` or :class:`memoryview`.
401
+ mode: The permissions to set on the file. Defaults to 0o644 (-rw-r--r--) for new files.
402
+ If the file already exists, its permissions will be changed,
403
+ unless ``mode`` is ``None`` (default).
404
+ user: The name of the user to set for the directory.
405
+ If ``group`` isn't provided, the user's default group is used.
406
+ If the file already exists, its user and group will be changed,
407
+ unless ``user`` is ``None`` (default).
408
+ group: The name of the group to set for the directory. For :class:`ContainerPath`,
409
+ it is an error to provide ``group`` if ``user`` isn't provided.
410
+ If the file already exists, its group will be changed,
411
+ unless ``user`` and ``group`` are ``None`` (default).
412
+
413
+ Returns:
414
+ The number of bytes written.
415
+
416
+ Raises:
417
+ FileNotFoundError: if the parent directory does not exist.
418
+ LookupError: if the user or group is unknown, or a group is provided without a user.
419
+ NotADirectoryError: if the parent exists as a non-directory file.
420
+ PermissionError: if the user does not have permissions for the operation.
421
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
422
+ """
423
+ ...
424
+
425
+ # NOTE: Not supported -- (Python 3.10) ``newline`` argument
426
+ def write_text(
427
+ self,
428
+ data: str,
429
+ *,
430
+ mode: int | None = None,
431
+ user: str | None = None,
432
+ group: str | None = None,
433
+ ) -> int:
434
+ """Write the provided string to the corresponding path.
435
+
436
+ Compared to :meth:`pathlib.Path.write_text`, this method drops the ``encoding`` and
437
+ ``errors`` args to simplify the API. This method adds ``mode``, ``user`` and ``group``
438
+ args, which are set on file creation.
439
+
440
+ Args:
441
+ data: The string to write. Newlines are not modified on writing.
442
+ mode: The permissions to set on the file. Defaults to 0o644 (-rw-r--r--) for new files.
443
+ If the file already exists, its permissions will be changed,
444
+ unless ``mode`` is ``None`` (default).
445
+ user: The name of the user to set for the directory.
446
+ If ``group`` isn't provided, the user's default group is used.
447
+ If the file already exists, its user and group will be changed,
448
+ unless ``user`` is ``None`` (default).
449
+ group: The name of the group to set for the directory. For :class:`ContainerPath`,
450
+ it is an error to provide ``group`` if ``user`` isn't provided.
451
+ If the file already exists, its group will be changed,
452
+ unless ``user`` and ``group`` are ``None`` (default).
453
+
454
+ Returns:
455
+ The number of bytes written.
456
+
457
+ Raises:
458
+ FileNotFoundError: if the parent directory does not exist.
459
+ LookupError: if the user or group is unknown, or a group is provided without a user.
460
+ NotADirectoryError: if the parent exists as a non-directory file.
461
+ PermissionError: if the user does not have permissions for the operation.
462
+ UnicodeError: if the provided data is not valid UTF-8.
463
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
464
+ """
465
+ ...
466
+
467
+ def mkdir(
468
+ self,
469
+ mode: int = _constants.DEFAULT_MKDIR_MODE,
470
+ parents: bool = False,
471
+ exist_ok: bool = False,
472
+ *,
473
+ user: str | None = None,
474
+ group: str | None = None,
475
+ ) -> None:
476
+ """Create a new directory at the corresponding path.
477
+
478
+ Compared to :meth:`pathlib.Path.mkdir`, this method adds ``user`` and ``group`` args.
479
+ These are used to set the ownership of the created directory. Any created parents
480
+ will not have their ownership set.
481
+
482
+ Args:
483
+ mode: The permissions to set on the created directory. Any parents created will have
484
+ their permissions set to the default value of 0o755 (drwxr-xr-x).
485
+ The permissions are not changed if the directory already exists.
486
+ parents: Whether to create any missing parent directories as well. If ``False``
487
+ (default) and a parent directory does not exist, a :class:`FileNotFound` error will
488
+ be raised.
489
+ exist_ok: Whether to raise an error if the directory already exists.
490
+ If ``False`` (default) and the directory already exists,
491
+ a :class:`FileExistsError` will be raised.
492
+ user: The name of the user to set for the directory.
493
+ If ``group`` isn't provided, the user's default group is used.
494
+ The user and group are not changed if the directory already exists.
495
+ group: The name of the group to set for the directory. For :class:`ContainerPath`,
496
+ it is an error to provide ``group`` if ``user`` isn't provided.
497
+ The user and group are not changed if the directory already exists.
498
+
499
+ Raises:
500
+ FileExistsError: if the directory already exists and ``exist_ok`` is ``False``.
501
+ FileNotFoundError: if the parent directory does not exist and ``parents`` is ``False``.
502
+ LookupError: if the user or group is unknown, or a group is provided without a user.
503
+ NotADirectoryError: if the parent exists as a non-directory file.
504
+ PermissionError: if the local user does not have permissions for the operation.
505
+ PebbleConnectionError: if the remote Pebble client cannot be reached.
506
+ """
507
+ ...
508
+
509
+
510
+ ################################################
511
+ # pathlib methods not included in the protocol #
512
+ ################################################
513
+
514
+ # TODO: clean up these comments structurally and informationally
515
+
516
+ #############################
517
+ # protocol PurePath methods #
518
+ #############################
519
+
520
+ # constructors
521
+ # ContainerPath constructor will differ from pathlib.Path constructor
522
+ # not part of the protocol
523
+ # def __new__(cls, *args: _StrPath, **kwargs: object) -> Self: ...
524
+ # __new__ signature is version dependent
525
+ # def __init__(self, *args): ...
526
+
527
+ # def __reduce__(self): ...
528
+ # ops.Container isn't pickleable itself, but we can provide a custom constructor
529
+ # for ContainerPath that works with the unpickling protocol, and will attempt to
530
+ # make a Container object connected to the appropriate pebble socket
531
+ # for simplicity this will be omitted from v1 unless requested
532
+
533
+ # def __rtruediv__(self, key: str | os.PathLike[str]) -> Self: ...
534
+ # omitted from v1 protocol
535
+ # doesn't seem worth supporting until (if) ContainerPath gets relative paths
536
+ #
537
+ # when we have relative paths, it will be meaningful to support the following:
538
+ # def __truediv__(self, key: str | os.PathLike[str] | Self) -> Self: ...
539
+ # def __rtruediv__(self, key: str | os.PathLike[str] | Self) -> Self: ...
540
+ # `ContainerPath / (str or pathlib.Path)`, or `(str or pathlib.Path) / containerPath`
541
+ # will result in a new ContainerPath with the same container.
542
+ # `ContainerPath / ContainerPath` is an error if the containers are not the same,
543
+ # otherwise it too results in a new ContainerPath with the same container.
544
+
545
+ # def __fspath__(self) -> str: ...
546
+ # we don't want ContainerPath to be os.PathLike
547
+
548
+ # def __bytes__(self) -> bytes: ...
549
+ # we don't want ContainerPath to be mistakenly used like a pathlib.Path
550
+
551
+ # URIs
552
+ # this doesn't seem useful and is potentially confusing,so it won't be implemented
553
+ # likewise, the constructor (added in 3.13) won't be implemented
554
+ # def as_uri(self) -> str: ...
555
+ # @classmethod
556
+ # def from_uri(uri: str) -> Self: ...
557
+
558
+ # def is_reserved(self) -> bool: ...
559
+ # this will always return False with a PosixPath. Since we assume a Linux container
560
+ # so let's just drop it from the protocol for now
561
+
562
+ # def full_match(self, pattern: str, * case_sensitive: bool = False) -> bool: ...
563
+ # 3.13+
564
+ # not part of the protocol but may eventually be provided on ContainerPath
565
+ # to ease compatibility with pathlib.Path on 3.13+
566
+
567
+ # def relative_to(self, other: _StrPath, /) -> Self: ...
568
+ # this produces relative paths, which we shouldn't be using in any code designed to
569
+ # be compatible with both machines and containers, since pebble will error on any
570
+ # relative paths at runtime -- if users want to work with relative paths, I think
571
+ # they should explicitly work with a PurePath rather than a ContainerPath, and then
572
+ # construct the ContainerPath when they have the absolute path they need
573
+ #
574
+ # Python 3.12 deprecates the below signature, to be dropped in 3.14
575
+ # def relative_to(self, *other: _StrPath) -> Self: ...
576
+ # to ease future compatibility, we'd just drop support for the old signature
577
+ # from the protocol now if it was included
578
+ #
579
+ # Python 3.12 further modifies the signature with an additional keyword argument
580
+ # def relative_to(self, other: _StrPath, walk_up: bool = False) -> Self: ...
581
+ # this would not part of the protocol but could eventually be provided on
582
+ # ContainerPath to ease compatibility with pathlib.Path on 3.12+ if we someday
583
+ # support relative paths
584
+
585
+ # def is_relative_to(self, other: _StrPath) -> Self: ... # 3.9+
586
+ # not part of protocol but can be provided on ContainerPath implementation
587
+ # to ease compatibility with pathlib.Path on 3.9+
588
+
589
+ # def with_stem(self, stem: str) -> Self: ... # 3.9+
590
+ # not part of protocol but can be provided on ContainerPath implementation
591
+ # to ease compatibility with pathlib.Path on 3.9+
592
+ # could be added to the protocol if we're happy for LocalPath to double as backports
593
+
594
+ # def with_segments(self, *pathsegments: _StrPath) -> Self: ...
595
+ # required for 3.12+ subclassing machinery
596
+ # not part of the protocol (otherwise LocalPath would have to backport it)
597
+ # but it is a useful method -- given a ContainerPath with some container,
598
+ # you can make another path with the same container cleanly, so it'll be implemented
599
+ # on ContainerPath
600
+
601
+ # @property
602
+ # def drive(self) -> str: ...
603
+ # will always be '' for Posix -- maybe drop it from the protocol
604
+ # so users get more useful autocompletions?
605
+
606
+ # @property
607
+ # def root(self) -> str: ...
608
+ # potentially error prone -- ContainerPath.root / Path('foo') is not a ContainerPath
609
+
610
+ # @property
611
+ # def anchor(self) -> str: ...
612
+ # this is drive + root
613
+
614
+ #########################
615
+ # protocol Path methods #
616
+ #########################
617
+
618
+ # recursive glob is problematic because Pebble doesn't tell us whether something is a symlink
619
+ # so we can easily recurse until we hit Pebble's api limit
620
+ # def rglob(
621
+ # self,
622
+ # pattern: str, # support for _StrPath added in 3.13
623
+ # # *,
624
+ # # case_sensitive: bool = False, # added in 3.12
625
+ # # recurse_symlinks: bool = False, # added in 3.13
626
+ # ) -> Generator[Self]: ...
627
+
628
+ # walk was only added in 3.12 -- let's not support this for now, as we'd need to
629
+ # implement the walk logic for LocalPath as well as whatever we do for ContainerPath
630
+ # (which will also be a bit trickier being unable to distinguish symlinks as dirs)
631
+ # While Path.walk wraps os.walk, there are still ~30 lines of pathlib code we'd need
632
+ # to vendor for LocalPath.walk
633
+ # def walk(
634
+ # self,
635
+ # top_down: bool = True,
636
+ # on_error: typing.Callable[[OSError], None] | None = None,
637
+ # follow_symlinks: bool = False, # NOTE: ContainerPath runtime error if True
638
+ # ) -> typing.Iterator[tuple[Self, list[str], list[str]]]:
639
+ # # TODO: if we add a follow_symlinks option to Pebble's list_files API, we can
640
+ # # then support follow_symlinks=True on supported Pebble (Juju) versions
641
+ # ...
642
+
643
+ # def stat(self) -> os.stat_result: ...
644
+ # stat follows symlinks to return information about the target
645
+ # this may not be in v1, because we can only provide best effort completion on the
646
+ # pebble side -- for example, we can't distinguish block and char devices, and we can't
647
+ # detect if something is a symlink. Maybe we can provide a top-level fileinfo helper
648
+
649
+ # def lstat(self) -> os.stat_result: ...
650
+ # lstat tells you about the symlink rather than its target
651
+ # pebble only tells you about the target
652
+ # TODO: support if we add follow_symlinks to Pebble's list_files API
653
+
654
+ # def is_mount(self) -> bool: ...
655
+ # pebble doesn't support this
656
+
657
+ # def is_junction(self) -> bool: ...
658
+ # 3.12
659
+ # this will always be False in ContainerPath since we assume a Linux container
660
+ # so let's just drop it from the protocol for now
661
+
662
+ # is_block_device and is_char_device are problematic because pebble only tells us if
663
+ # it's a device at all. We can provide an is_device module level helper if needed.
664
+ # def is_block_device(self) -> bool: ...
665
+ # def is_char_device(self) -> bool: ...
666
+
667
+ ################################################################################
668
+ # these concrete methods are currently ruled out due to lack of Pebble support #
669
+ ################################################################################
670
+
671
+ # def chmod
672
+ # pebble sets mode on creation
673
+ # can't provide a separate method
674
+ # needs to be argument for other functions
675
+ # (same treatment needed for chown)
676
+
677
+ # link creation, modification, target retrieval
678
+ # pebble doesn't support link manipulation
679
+ # def hardlink_to
680
+ # def symlink_to
681
+ # def lchmod
682
+ # def readlink
683
+ # def resolve
684
+
685
+ # def samefile
686
+ # pebble doesn't return device and i-node number
687
+ # can't provide the same semantics
688
+
689
+ # def open
690
+ # the semantics would be different due to needing to make a local copy
691
+
692
+ # def touch
693
+ # would have to pull down the existing file and push it back up just to set mtime
694
+
695
+ ##################
696
+ # relative paths #
697
+ ##################
698
+
699
+ # OPINION: we shouldn't support relative paths in v1 (if ever)
700
+ #
701
+ # if we support relative paths, we'd need to implicitly call absolute before every
702
+ # call that goes to pebble, and it's not clear whether it's a good idea to implement
703
+ # cwd, which absolute would depend on -- we'd have to pebble exec cwd, which wouldn't
704
+ # work in certain images (bare rocks)
705
+ #
706
+ # I think it would be fine for v1 to only support absolute paths, raising an error
707
+ # on file operations with relative paths
708
+
709
+ # the following methods would require us to either hardcode cwd or use a pebble.exec
710
+ # def cwd
711
+ # typically /root in container
712
+ # do we need to query this each time? can we hardcode it?
713
+ # def absolute
714
+ # interpret relative to cwd
715
+
716
+ # the following methods would require us to either hardcode home or use a pebble.exec
717
+ # def home
718
+ # typically /root in container
719
+ # do we need to query this each time? can we hardcode it?
720
+ # def expanduser
721
+ # '~' in parts becomes self.home