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.
- charmlibs/pathops/__init__.py +54 -0
- charmlibs/pathops/_constants.py +31 -0
- charmlibs/pathops/_container_path.py +758 -0
- charmlibs/pathops/_errors.py +120 -0
- charmlibs/pathops/_fileinfo.py +97 -0
- charmlibs/pathops/_functions.py +104 -0
- charmlibs/pathops/_local_path.py +224 -0
- charmlibs/pathops/_types.py +721 -0
- charmlibs/pathops/_version.txt +1 -0
- charmlibs/pathops/py.typed +0 -0
- charmlibs_pathops-0.0.0.dist-info/METADATA +27 -0
- charmlibs_pathops-0.0.0.dist-info/RECORD +14 -0
- charmlibs_pathops-0.0.0.dist-info/WHEEL +5 -0
- charmlibs_pathops-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -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
|