iker-python-common 1.0.69__tar.gz → 1.0.71__tar.gz

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.
Files changed (71) hide show
  1. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/PKG-INFO +1 -1
  2. iker_python_common-1.0.71/src/iker/common/utils/shutils.py +320 -0
  3. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker_python_common.egg-info/PKG-INFO +1 -1
  4. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/csvutils_test.py +77 -0
  5. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/shutils_test.py +88 -72
  6. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/typeutils_test.py +5 -1
  7. iker_python_common-1.0.69/src/iker/common/utils/shutils.py +0 -239
  8. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/.editorconfig +0 -0
  9. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/.github/workflows/pr.yml +0 -0
  10. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/.github/workflows/push.yml +0 -0
  11. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/.gitignore +0 -0
  12. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/MANIFEST.in +0 -0
  13. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/README.md +0 -0
  14. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/VERSION +0 -0
  15. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/pyproject.toml +0 -0
  16. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/config/config.cfg +0 -0
  17. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/csvutils/data.csv +0 -0
  18. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/csvutils/data.tsv +0 -0
  19. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.baz/file.bar.baz +0 -0
  20. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.baz/file.foo.bar +0 -0
  21. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.baz/file.foo.baz +0 -0
  22. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.foo/dir.foo.bar/dir.foo.bar.baz/file.foo.bar.baz +0 -0
  23. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.bar.baz +0 -0
  24. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.bar +0 -0
  25. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.foo/dir.foo.bar/file.foo.baz +0 -0
  26. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.foo/file.bar +0 -0
  27. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.foo/file.baz +0 -0
  28. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/resources/unittest/shutils/dir.foo/file.foo +0 -0
  29. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/setup.cfg +0 -0
  30. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/setup.py +0 -0
  31. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/__init__.py +0 -0
  32. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/__init__.py +0 -0
  33. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/argutils.py +0 -0
  34. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/config.py +0 -0
  35. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/csvutils.py +0 -0
  36. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/dbutils.py +0 -0
  37. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/dtutils.py +0 -0
  38. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/funcutils.py +0 -0
  39. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/iterutils.py +0 -0
  40. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/jsonutils.py +0 -0
  41. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/logger.py +0 -0
  42. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/numutils.py +0 -0
  43. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/randutils.py +0 -0
  44. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/retry.py +0 -0
  45. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/span.py +0 -0
  46. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/strutils.py +0 -0
  47. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/testutils.py +0 -0
  48. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker/common/utils/typeutils.py +0 -0
  49. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker_python_common.egg-info/SOURCES.txt +0 -0
  50. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker_python_common.egg-info/dependency_links.txt +0 -0
  51. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker_python_common.egg-info/not-zip-safe +0 -0
  52. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker_python_common.egg-info/requires.txt +0 -0
  53. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/src/iker_python_common.egg-info/top_level.txt +0 -0
  54. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/__init__.py +0 -0
  55. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/__init__.py +0 -0
  56. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/__init__.py +0 -0
  57. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/argutils_test.py +0 -0
  58. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/config_test.py +0 -0
  59. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/dbutils_test.py +0 -0
  60. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/dtutils_test.py +0 -0
  61. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/funcutils_test.py +0 -0
  62. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/iterutils_test.py +0 -0
  63. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/jsonutils_test.py +0 -0
  64. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/logger_test.py +0 -0
  65. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/numutils_test.py +0 -0
  66. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/randutils_test.py +0 -0
  67. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/retry_test.py +0 -0
  68. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/span_test.py +0 -0
  69. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/strutils_test.py +0 -0
  70. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/iker_tests/common/utils/testutils_test.py +0 -0
  71. {iker_python_common-1.0.69 → iker_python_common-1.0.71}/test/testenv.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.69
3
+ Version: 1.0.71
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -0,0 +1,320 @@
1
+ import fnmatch
2
+ import os
3
+ import pathlib
4
+ import shutil
5
+ import sys
6
+ from collections.abc import Generator
7
+ from typing import Protocol
8
+
9
+ from iker.common.utils.strutils import is_empty
10
+
11
+ __all__ = [
12
+ "make_path",
13
+ "fn_suffix",
14
+ "fn_suffixes",
15
+ "fn_stem",
16
+ "path_depth",
17
+ "glob_match",
18
+ "scan_files",
19
+ "copy_files",
20
+ "run",
21
+ "execute",
22
+ ]
23
+
24
+
25
+ def make_path(
26
+ path: str | os.PathLike[str] | None,
27
+ *,
28
+ expand: bool = False,
29
+ normalize: bool = False,
30
+ absolute: bool = False,
31
+ resolve: bool = False,
32
+ check_exists: bool = False,
33
+ check_is_dir: bool = False,
34
+ check_is_file: bool = False,
35
+ ) -> pathlib.Path | None:
36
+ """
37
+ Creates a ``pathlib.Path`` object from the given path string or path-like object, applying the specified
38
+ transformations and checks.
39
+
40
+ :param path: The path string or path-like object to convert to a ``pathlib.Path`` object.
41
+ :param expand: If ``True``, expands environment variables and user home directory in the path.
42
+ :param normalize: If ``True``, normalizes the path by collapsing redundant separators and up-level references.
43
+ :param absolute: If ``True``, converts the path to an absolute path.
44
+ :param resolve: If ``True``, resolves symbolic links and returns the canonical path.
45
+ :param check_exists: If ``True``, checks if the path exists and raises a ``FileNotFoundError`` if it does not.
46
+ :param check_is_dir: If ``True``, checks if the path is a directory and raises a ``NotADirectoryError`` if it is not.
47
+ :param check_is_file: If ``True``, checks if the path is a file and raises a ``FileNotFoundError`` if it is not.
48
+ :return: A ``pathlib.Path`` object representing the given path after applying the specified transformations and
49
+ checks, or ``None`` if the input path is ``None``.
50
+ """
51
+ if path is None:
52
+ return None
53
+ path = os.fspath(path)
54
+
55
+ if expand:
56
+ path = os.path.expandvars(path)
57
+ path = os.path.expanduser(path)
58
+ if normalize:
59
+ path = os.path.normpath(path)
60
+ if absolute:
61
+ path = os.path.abspath(path)
62
+ if resolve:
63
+ path = os.path.realpath(path)
64
+
65
+ path = pathlib.Path(path)
66
+
67
+ if check_exists and not path.exists():
68
+ raise FileNotFoundError(f"path does not exist '{path}'")
69
+ if check_is_dir and not path.is_dir():
70
+ raise NotADirectoryError(f"path is not a directory '{path}'")
71
+ if check_is_file and not path.is_file():
72
+ raise FileNotFoundError(f"path is not a file '{path}'")
73
+
74
+ return path
75
+
76
+
77
+ if sys.version_info < (3, 14):
78
+ def fn_suffix(filename: str | os.PathLike[str] | None) -> str | None:
79
+ """
80
+ Extracts the filename suffix from the given filename or path.
81
+
82
+ :param filename: The specific filename or path.
83
+ :return: The filename suffix, including the leading dot (e.g., ".txt" for "file.txt"),
84
+ or ``None`` if the input filename is ``None``.
85
+ """
86
+ if (path := make_path(filename)) is not None:
87
+ _, suffix_name = os.path.splitext(path.name)
88
+ return suffix_name
89
+ return None
90
+ else:
91
+ def fn_suffix(filename: str | os.PathLike[str] | None) -> str | None:
92
+ """
93
+ Extracts the filename suffix from the given filename or path.
94
+
95
+ :param filename: The specific filename or path.
96
+ :return: The filename suffix, including the leading dot (e.g., ".txt" for "file.txt"),
97
+ or ``None`` if the input filename is ``None``.
98
+ """
99
+ if (path := make_path(filename)) is not None:
100
+ return path.suffix
101
+ return None
102
+
103
+ if sys.version_info < (3, 14):
104
+ def fn_suffixes(filename: str | os.PathLike[str] | None) -> list[str]:
105
+ """
106
+ Extracts all filename suffixes from the given filename or path.
107
+
108
+ :param filename: The specific filename or path.
109
+ :return: A list of filename suffixes, each including the leading dot (e.g., [".tar", ".gz"] for "archive.tar.gz").
110
+ """
111
+ if (path := make_path(filename)) is not None:
112
+ results = []
113
+ stem_name = path.name
114
+ while True:
115
+ stem_name, suffix_name = os.path.splitext(stem_name)
116
+ if is_empty(suffix_name):
117
+ return list(reversed(results))
118
+ results.append(suffix_name)
119
+ return []
120
+ else:
121
+ def fn_suffixes(filename: str | os.PathLike[str] | None) -> list[str]:
122
+ """
123
+ Extracts all filename suffixes from the given filename or path.
124
+
125
+ :param filename: The specific filename or path.
126
+ :return: A list of filename suffixes, each including the leading dot (e.g., [".tar", ".gz"] for "archive.tar.gz").
127
+ """
128
+ if (path := make_path(filename)) is not None:
129
+ return path.suffixes
130
+ return []
131
+
132
+ if sys.version_info < (3, 14):
133
+ def fn_stem(filename: str | os.PathLike[str] | None, minimal: bool = False) -> str | None:
134
+ """
135
+ Extracts the filename stem from the given filename or path.
136
+
137
+ :param filename: The specific filename or path.
138
+ :param minimal: If ``True``, extracts the minimal (shortest) stem, otherwise the default stem.
139
+ :return: The filename stem.
140
+ """
141
+ if (path := make_path(filename)) is not None:
142
+ if not minimal:
143
+ stem_name, _ = os.path.splitext(path.name)
144
+ return stem_name
145
+ else:
146
+ stem_name = path.name
147
+ while True:
148
+ stem_name, suffix_name = os.path.splitext(stem_name)
149
+ if is_empty(suffix_name):
150
+ return stem_name
151
+ return None
152
+ else:
153
+ def fn_stem(filename: str | os.PathLike[str] | None, minimal: bool = False) -> str | None:
154
+ """
155
+ Extracts the filename stem from the given filename or path.
156
+
157
+ :param filename: The specific filename or path.
158
+ :param minimal: If ``True``, extracts the minimal (shortest) stem, otherwise the default stem.
159
+ :return: The filename stem.
160
+ """
161
+ if (path := make_path(filename)) is not None:
162
+ if not minimal:
163
+ return path.stem
164
+ else:
165
+ while True:
166
+ if is_empty(path.suffix):
167
+ return path.stem
168
+ path = path.with_suffix("")
169
+ return None
170
+
171
+
172
+ def path_depth(root: str | os.PathLike[str], child: str | os.PathLike[str]) -> int:
173
+ """
174
+ Returns the relative path depth from the given ``child`` to the ``root``.
175
+
176
+ :param root: The root path.
177
+ :param child: The child path.
178
+ :return: Relative depth, or -1 if ``child`` is not under ``root``.
179
+ """
180
+ root = make_path(root, expand=True, resolve=True)
181
+ child = make_path(child, expand=True, resolve=True)
182
+ if not child.is_relative_to(root):
183
+ return -1
184
+ return len(child.relative_to(root).parts)
185
+
186
+
187
+ def glob_match(
188
+ names: list[str],
189
+ include_patterns: list[str] | None = None,
190
+ exclude_patterns: list[str] | None = None,
191
+ ) -> list[str]:
192
+ """
193
+ Applies the given inclusive and exclusive glob patterns to the given ``names`` and returns the filtered result.
194
+
195
+ :param names: Names to apply the glob patterns to.
196
+ :param include_patterns: Inclusive glob patterns.
197
+ :param exclude_patterns: Exclusive glob patterns.
198
+ :return: Filtered names matching the patterns.
199
+ """
200
+ ret = set()
201
+ for pat in (include_patterns or []):
202
+ ret.update(fnmatch.filter(names, pat))
203
+ if include_patterns is None or len(include_patterns) == 0:
204
+ ret.update(names)
205
+ for pat in (exclude_patterns or []):
206
+ ret.difference_update(fnmatch.filter(names, pat))
207
+ return list(ret)
208
+
209
+
210
+ class CopyFuncProtocol(Protocol):
211
+ def __call__(self, src: str | os.PathLike[str], dst: str | os.PathLike[str], **kwargs) -> None: ...
212
+
213
+
214
+ def scan_files(
215
+ path: str | os.PathLike[str],
216
+ *,
217
+ include_patterns: list[str] | None = None,
218
+ exclude_patterns: list[str] | None = None,
219
+ depth: int = 0,
220
+ ) -> Generator[pathlib.Path, None, None]:
221
+ """
222
+ Recursively scans the given ``path`` and returns a list of files whose names satisfy the given name patterns and the
223
+ relative depth of their folders to the given root path is not greater than the specified ``depth`` value.
224
+
225
+ :param path: The root path to scan.
226
+ :param include_patterns: Inclusive glob patterns applied to the filenames.
227
+ :param exclude_patterns: Exclusive glob patterns applied to the filenames.
228
+ :param depth: Maximum depth of the subdirectories included in the scan.
229
+ :return: A generator yielding the paths of the files matching the patterns.
230
+ """
231
+ path = make_path(path, expand=True)
232
+ if path.exists() and not path.is_dir():
233
+ if len(glob_match([path.name], include_patterns, exclude_patterns)) == 0:
234
+ return
235
+ yield path
236
+
237
+ for parent, dirs, filenames in path.walk():
238
+ if 0 < depth <= path_depth(path, parent):
239
+ continue
240
+ for filename in glob_match(filenames, include_patterns, exclude_patterns):
241
+ yield parent / filename
242
+
243
+
244
+ def copy_files(
245
+ src: str | os.PathLike[str],
246
+ dst: str | os.PathLike[str],
247
+ *,
248
+ include_patterns: list[str] | None = None,
249
+ exclude_patterns: list[str] | None = None,
250
+ depth: int = 0,
251
+ follow_symlinks: bool = False,
252
+ ignore_dangling_symlinks: bool = False,
253
+ dirs_exist_ok: bool = False,
254
+ copy_func: CopyFuncProtocol = shutil.copy2
255
+ ):
256
+ """
257
+ Recursively copies the given source path to the destination path. Only copies the files whose names satisfy the
258
+ given name patterns and the relative depth of their folders to the given source path is not greater than the
259
+ specified ``depth`` value.
260
+
261
+ :param src: The source path or file.
262
+ :param dst: The destination path or file.
263
+ :param include_patterns: Inclusive glob patterns applied to the filenames.
264
+ :param exclude_patterns: Exclusive glob patterns applied to the filenames.
265
+ :param depth: Maximum depth of the subdirectories included in the scan.
266
+ :param follow_symlinks: If ``True``, create symbolic links for the symbolic links present in the source; otherwise,
267
+ make a physical copy.
268
+ :param ignore_dangling_symlinks: If ``True``, ignore errors if the file pointed by the symbolic link does not exist.
269
+ :param dirs_exist_ok: If ``True``, ignore errors if the destination directory and subdirectories exist.
270
+ :param copy_func: Copy function to use for copying files.
271
+ """
272
+ src = make_path(src, expand=True)
273
+ dst = make_path(dst, expand=True)
274
+ if not src.is_dir():
275
+ if len(glob_match([src.name], include_patterns, exclude_patterns)) == 0:
276
+ return
277
+ if not dst.exists():
278
+ dst.parent.mkdir(parents=True, exist_ok=True)
279
+ copy_func(src, dst, follow_symlinks=follow_symlinks)
280
+ return
281
+
282
+ def ignore_func(parent: str | os.PathLike[str], names: list[str]) -> set[str]:
283
+ parent = make_path(parent, expand=True)
284
+ filenames = list(filter(lambda x: not (parent / x).is_dir(), names))
285
+ ret = set(filenames)
286
+ if 0 < depth <= path_depth(src, parent):
287
+ return ret
288
+ ret.difference_update(glob_match(filenames, include_patterns, exclude_patterns))
289
+ return ret
290
+
291
+ shutil.copytree(src,
292
+ dst,
293
+ symlinks=follow_symlinks,
294
+ ignore=ignore_func,
295
+ ignore_dangling_symlinks=ignore_dangling_symlinks,
296
+ dirs_exist_ok=dirs_exist_ok,
297
+ copy_function=copy_func)
298
+
299
+
300
+ def run(cmd: str) -> bool:
301
+ """
302
+ Runs the given command and returns the success status.
303
+
304
+ :param cmd: Command to run.
305
+ :return: ``True`` if the command has been successfully run, ``False`` otherwise.
306
+ """
307
+ return os.system(cmd) == 0
308
+
309
+
310
+ def execute(cmd: str, strip: bool = True) -> str:
311
+ """
312
+ Executes the given command and returns contents collected from standard output.
313
+
314
+ :param cmd: Command to execute.
315
+ :param strip: If ``True``, the contents will be stripped.
316
+ :return: The content from standard output.
317
+ """
318
+ if strip:
319
+ return os.popen(cmd).read().strip()
320
+ return os.popen(cmd).read()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: iker-python-common
3
- Version: 1.0.69
3
+ Version: 1.0.71
4
4
  Classifier: Programming Language :: Python :: 3
5
5
  Classifier: Programming Language :: Python :: 3.12
6
6
  Classifier: Programming Language :: Python :: 3.13
@@ -1,5 +1,6 @@
1
1
  import itertools
2
2
  import math
3
+ import tempfile
3
4
  import unittest
4
5
 
5
6
  import ddt
@@ -179,6 +180,72 @@ class CSVTest(unittest.TestCase):
179
180
  lines + lines + lines_no_header + lines_no_header):
180
181
  self.assertTrue(json_compare(a, e))
181
182
 
183
+ def test_lines__wrong_cols(self):
184
+ view = csv.view(
185
+ [
186
+ csv.column("dummy_str", loader=str, dumper=str, null_str=r"\N"),
187
+ csv.column("dummy_bool", loader=parse_bool, dumper=str, null_str=r"\N"),
188
+ csv.column("dummy_int", loader=int, dumper=str, null_str=r"\N"),
189
+ csv.column("dummy_float", loader=float, dumper=str, null_str=r"\N"),
190
+ ],
191
+ )
192
+
193
+ lines = [
194
+ ["dummy_str", "dummy_bool", "dummy_int", "dummy_float", "dummy_datetime", "dummy_params"],
195
+ ["foo", "True", "1", "1.0", "2020-01-01T00:00:00", ""],
196
+ ["bar", "False", "-1", "-1.0", "2020-01-01T00:00:00", "key_1=value_1;key_2;!key_3"],
197
+ ["baz", r"\N", "100", "inf", "2020-01-01T00:00:00", r"\N"],
198
+ ["", r"\N", "-100", "-inf", "2020-01-01T00:00:00", ""],
199
+ [r"\N", r"\N", "0", "nan", "2020-01-01T00:00:00", r"\N"],
200
+ [r"\N", r"\N", r"\N", r"\N", r"\N", r"\N"],
201
+ ]
202
+
203
+ with self.assertRaises(ValueError):
204
+ list(view.load_lines(lines, has_header=True))
205
+
206
+ data = [
207
+ ["foo", "True", "1", "1.0", "2020-01-01T00:00:00", ""],
208
+ ["bar", "False", "-1", "-1.0", "2020-01-01T00:00:00", "key_1=value_1;key_2;!key_3"],
209
+ ["baz", r"\N", "100", "inf", "2020-01-01T00:00:00", r"\N"],
210
+ ["", r"\N", "-100", "-inf", "2020-01-01T00:00:00", ""],
211
+ [r"\N", r"\N", "0", "nan", "2020-01-01T00:00:00", r"\N"],
212
+ [r"\N", r"\N", r"\N", r"\N", r"\N", r"\N"],
213
+ ]
214
+
215
+ with self.assertRaises(ValueError):
216
+ list(view.dump_lines(data, has_header=True))
217
+
218
+ def test_lines__wrong_col_name(self):
219
+ view = csv.view(
220
+ [
221
+ csv.column("another_dummy_str", loader=str, dumper=str, null_str=r"\N"),
222
+ csv.column("another_dummy_bool", loader=parse_bool, dumper=str, null_str=r"\N"),
223
+ csv.column("another_dummy_int", loader=int, dumper=str, null_str=r"\N"),
224
+ csv.column("another_dummy_float", loader=float, dumper=str, null_str=r"\N"),
225
+ csv.column("another_dummy_datetime",
226
+ loader=lambda x: dt_to_ts(dt_parse_iso(x)),
227
+ dumper=lambda x: dt_format_iso(dt_from_ts(x)),
228
+ null_str=r"\N"),
229
+ csv.column("another_dummy_params",
230
+ loader=lambda x: parse_params_string(x, delim=";", kv_delim="=", neg_prefix="!"),
231
+ dumper=lambda x: make_params_string(x, delim=";", kv_delim="=", neg_prefix="!"),
232
+ null_str=r"\N"),
233
+ ],
234
+ )
235
+
236
+ lines = [
237
+ ["dummy_str", "dummy_bool", "dummy_int", "dummy_float", "dummy_datetime", "dummy_params"],
238
+ ["foo", "True", "1", "1.0", "2020-01-01T00:00:00", ""],
239
+ ["bar", "False", "-1", "-1.0", "2020-01-01T00:00:00", "key_1=value_1;key_2;!key_3"],
240
+ ["baz", r"\N", "100", "inf", "2020-01-01T00:00:00", r"\N"],
241
+ ["", r"\N", "-100", "-inf", "2020-01-01T00:00:00", ""],
242
+ [r"\N", r"\N", "0", "nan", "2020-01-01T00:00:00", r"\N"],
243
+ [r"\N", r"\N", r"\N", r"\N", r"\N", r"\N"],
244
+ ]
245
+
246
+ with self.assertRaises(ValueError):
247
+ list(view.load_lines(lines, has_header=True))
248
+
182
249
  def test_file(self):
183
250
  view_csv = csv.view(
184
251
  [
@@ -236,3 +303,13 @@ class CSVTest(unittest.TestCase):
236
303
  self.assertTrue(json_compare(c, d))
237
304
  self.assertTrue(json_compare(t, d))
238
305
  self.assertTrue(json_compare(c, t))
306
+
307
+ with tempfile.NamedTemporaryFile() as fh:
308
+ view_csv.dump_file(data, fh.name, has_header=True, encoding="utf-8")
309
+ for a, e in zip(view_csv.load_file(fh.name, has_header=True, encoding="utf-8"), data):
310
+ self.assertTrue(json_compare(a, e))
311
+
312
+ with tempfile.NamedTemporaryFile() as fh:
313
+ view_tsv.dump_file(data, fh.name, has_header=True, encoding="utf-8")
314
+ for a, e in zip(view_tsv.load_file(fh.name, has_header=True, encoding="utf-8"), data):
315
+ self.assertTrue(json_compare(a, e))
@@ -5,10 +5,10 @@ import unittest
5
5
 
6
6
  import ddt
7
7
 
8
- from iker.common.utils.shutils import copy, listfile
9
- from iker.common.utils.shutils import expanded_path, norm_path, path_depth
10
- from iker.common.utils.shutils import extension, extensions, stem
8
+ from iker.common.utils.shutils import copy_files, scan_files
9
+ from iker.common.utils.shutils import fn_stem, fn_suffix, fn_suffixes
11
10
  from iker.common.utils.shutils import glob_match
11
+ from iker.common.utils.shutils import make_path, path_depth
12
12
  from testenv import resources_directory
13
13
 
14
14
  work_dir = pathlib.Path.cwd
@@ -17,7 +17,7 @@ home_dir = pathlib.Path.home
17
17
 
18
18
  @ddt.ddt
19
19
  class ShUtilsTest(unittest.TestCase):
20
- data_extension = [
20
+ data_fn_suffix = [
21
21
  (".", ""),
22
22
  ("..", ""),
23
23
  ("...", ""),
@@ -43,13 +43,44 @@ class ShUtilsTest(unittest.TestCase):
43
43
  ("s3://bucket/foo/bar/git.scrappy.ignored", ".ignored"),
44
44
  ]
45
45
 
46
- @ddt.idata(data_extension)
46
+ @ddt.idata(data_fn_suffix)
47
47
  @ddt.unpack
48
- def test_extension(self, data, expect):
49
- self.assertEqual(expect, extension(data))
48
+ def test_fn_suffix(self, data, expect):
49
+ self.assertEqual(expect, fn_suffix(data))
50
50
 
51
- data_stem = [
52
- (".", "."),
51
+ data_fn_suffixes = [
52
+ (".", []),
53
+ ("..", []),
54
+ ("...", []),
55
+ (".ignored", []),
56
+ (".ignored.", ["."]),
57
+ (".git.ignored", [".ignored"]),
58
+ ("..git.ignored", [".ignored"]),
59
+ ("..git..ignored", [".", ".ignored"]),
60
+ ("git.ignored", [".ignored"]),
61
+ ("git.scrappy.ignored", [".scrappy", ".ignored"]),
62
+ ("~/git.scrappy.ignored", [".scrappy", ".ignored"]),
63
+ ("./git.scrappy.ignored", [".scrappy", ".ignored"]),
64
+ ("../git.scrappy.ignored", [".scrappy", ".ignored"]),
65
+ ("/~/git.scrappy.ignored", [".scrappy", ".ignored"]),
66
+ ("/./git.scrappy.ignored", [".scrappy", ".ignored"]),
67
+ ("/../git.scrappy.ignored", [".scrappy", ".ignored"]),
68
+ ("/foo/bar/git.scrappy.ignored", [".scrappy", ".ignored"]),
69
+ ("/foo/bar/~/git.scrappy.ignored", [".scrappy", ".ignored"]),
70
+ ("/foo/bar/./git.scrappy.ignored", [".scrappy", ".ignored"]),
71
+ ("/foo/bar/../git.scrappy.ignored", [".scrappy", ".ignored"]),
72
+ ("/foo/bar/git.scrappy.ignored..baz.", [".scrappy", ".ignored", ".", ".baz", "."]),
73
+ ("http://domain/foo/bar/git.scrappy.ignored", [".scrappy", ".ignored"]),
74
+ ("s3://bucket/foo/bar/git.scrappy.ignored", [".scrappy", ".ignored"]),
75
+ ]
76
+
77
+ @ddt.idata(data_fn_suffixes)
78
+ @ddt.unpack
79
+ def test_fn_suffixes(self, data, expect):
80
+ self.assertEqual(expect, fn_suffixes(data))
81
+
82
+ data_fn_stem = [
83
+ (".", ""),
53
84
  ("..", ".."),
54
85
  ("...", "..."),
55
86
  (".ignored", ".ignored"),
@@ -74,13 +105,13 @@ class ShUtilsTest(unittest.TestCase):
74
105
  ("s3://bucket/foo/bar/git.scrappy.ignored", "git.scrappy"),
75
106
  ]
76
107
 
77
- @ddt.idata(data_stem)
108
+ @ddt.idata(data_fn_stem)
78
109
  @ddt.unpack
79
- def test_stem(self, data, expect):
80
- self.assertEqual(expect, stem(data))
110
+ def test_fn_stem(self, data, expect):
111
+ self.assertEqual(expect, fn_stem(data))
81
112
 
82
- data_stem__minimal = [
83
- (".", "."),
113
+ data_fn_stem__minimal = [
114
+ (".", ""),
84
115
  ("..", ".."),
85
116
  ("...", "..."),
86
117
  (".ignored", ".ignored"),
@@ -105,43 +136,12 @@ class ShUtilsTest(unittest.TestCase):
105
136
  ("s3://bucket/foo/bar/git.scrappy.ignored", "git"),
106
137
  ]
107
138
 
108
- @ddt.idata(data_stem__minimal)
139
+ @ddt.idata(data_fn_stem__minimal)
109
140
  @ddt.unpack
110
- def test_stem__minimal(self, data, expect):
111
- self.assertEqual(expect, stem(data, minimal=True))
141
+ def test_fn_stem__minimal(self, data, expect):
142
+ self.assertEqual(expect, fn_stem(data, minimal=True))
112
143
 
113
- data_extensions = [
114
- (".", []),
115
- ("..", []),
116
- ("...", []),
117
- (".ignored", []),
118
- (".ignored.", ["."]),
119
- (".git.ignored", [".ignored"]),
120
- ("..git.ignored", [".ignored"]),
121
- ("..git..ignored", [".ignored", "..ignored"]),
122
- ("git.ignored", [".ignored"]),
123
- ("git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
124
- ("~/git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
125
- ("./git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
126
- ("../git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
127
- ("/~/git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
128
- ("/./git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
129
- ("/../git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
130
- ("/foo/bar/git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
131
- ("/foo/bar/~/git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
132
- ("/foo/bar/./git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
133
- ("/foo/bar/../git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
134
- ("/foo/bar/git.scrappy.ignored..baz.", [".", ".baz.", "..baz.", ".ignored..baz.", ".scrappy.ignored..baz."]),
135
- ("http://domain/foo/bar/git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
136
- ("s3://bucket/foo/bar/git.scrappy.ignored", [".ignored", ".scrappy.ignored"]),
137
- ]
138
-
139
- @ddt.idata(data_extensions)
140
- @ddt.unpack
141
- def test_extensions(self, data, expect):
142
- self.assertEqual(expect, extensions(data))
143
-
144
- data_expanded_path = [
144
+ data_make_path = [
145
145
  (".", f"{work_dir()}"),
146
146
  ("./", f"{work_dir()}"),
147
147
  ("./foo", f"{work_dir()}/foo"),
@@ -186,12 +186,22 @@ class ShUtilsTest(unittest.TestCase):
186
186
  ("${EMPTY_ENV_PARAM}/foo/../bar/..", f"/"),
187
187
  ]
188
188
 
189
- @ddt.idata(data_expanded_path)
189
+ @ddt.idata(data_make_path)
190
190
  @ddt.unpack
191
- def test_expanded_path(self, data, expect):
191
+ def test_make_path(self, data, expect):
192
192
  os.environ["DUMMY_ENV_PARAM"] = "dummy_parent"
193
193
  os.environ["EMPTY_ENV_PARAM"] = ""
194
- self.assertEqual(norm_path(expect), norm_path(expanded_path(data)))
194
+
195
+ self.assertEqual(make_path(expect), make_path(expect, normalize=True))
196
+ self.assertEqual(make_path(expect), make_path(data, expand=True, normalize=True, absolute=True, resolve=True))
197
+
198
+ if not make_path(expect).exists():
199
+ with self.assertRaises(FileNotFoundError):
200
+ make_path(expect, check_exists=True)
201
+ with self.assertRaises(NotADirectoryError):
202
+ make_path(expect, check_is_dir=True)
203
+ with self.assertRaises(FileNotFoundError):
204
+ make_path(expect, check_is_file=True)
195
205
 
196
206
  data_path_depth = [
197
207
  (".", ".", 0),
@@ -272,10 +282,12 @@ class ShUtilsTest(unittest.TestCase):
272
282
  @ddt.idata(data_glob_match)
273
283
  @ddt.unpack
274
284
  def test_glob_match(self, names, include_patterns, exclude_patterns, expect):
275
- self.assertSetEqual(set(map(norm_path, glob_match(names, include_patterns, exclude_patterns))),
276
- set(map(norm_path, expect)))
285
+ self.assertSetEqual(
286
+ set(map(lambda x: make_path(x, normalize=True), glob_match(names, include_patterns, exclude_patterns))),
287
+ set(map(lambda x: make_path(x, normalize=True), expect)),
288
+ )
277
289
 
278
- data_listfile = [
290
+ data_scan_files = [
279
291
  (
280
292
  "unittest/shutils",
281
293
  [],
@@ -411,16 +423,18 @@ class ShUtilsTest(unittest.TestCase):
411
423
  ),
412
424
  ]
413
425
 
414
- @ddt.idata(data_listfile)
426
+ @ddt.idata(data_scan_files)
415
427
  @ddt.unpack
416
- def test_listfile(self, path, include_patterns, exclude_patterns, depth, expect):
417
- self.assertSetEqual(set(map(norm_path, listfile(resources_directory / path,
418
- include_patterns=include_patterns,
419
- exclude_patterns=exclude_patterns,
420
- depth=depth))),
421
- set(map(norm_path, map(lambda x: resources_directory / x, expect))))
428
+ def test_scan_files(self, path, include_patterns, exclude_patterns, depth, expect):
429
+ self.assertSetEqual(
430
+ set(map(lambda x: make_path(x, normalize=True), scan_files(resources_directory / path,
431
+ include_patterns=include_patterns,
432
+ exclude_patterns=exclude_patterns,
433
+ depth=depth))),
434
+ set(map(lambda x: make_path(x, normalize=True), map(lambda x: resources_directory / x, expect))),
435
+ )
422
436
 
423
- data_copy = [
437
+ data_copy_files = [
424
438
  (
425
439
  "unittest/shutils",
426
440
  "unittest/shutils",
@@ -568,16 +582,18 @@ class ShUtilsTest(unittest.TestCase):
568
582
  ),
569
583
  ]
570
584
 
571
- @ddt.idata(data_copy)
585
+ @ddt.idata(data_copy_files)
572
586
  @ddt.unpack
573
- def test_copy(self, src, dst, include_patterns, exclude_patterns, depth, expect):
587
+ def test_copy_files(self, src, dst, include_patterns, exclude_patterns, depth, expect):
574
588
  with tempfile.TemporaryDirectory() as temp_directory:
575
589
  temp_directory = pathlib.Path(temp_directory)
576
- copy(resources_directory / src,
577
- temp_directory / dst,
578
- include_patterns=include_patterns,
579
- exclude_patterns=exclude_patterns,
580
- depth=depth)
590
+ copy_files(resources_directory / src,
591
+ temp_directory / dst,
592
+ include_patterns=include_patterns,
593
+ exclude_patterns=exclude_patterns,
594
+ depth=depth)
581
595
 
582
- self.assertSetEqual(set(map(norm_path, listfile(temp_directory / dst))),
583
- set(map(norm_path, map(lambda x: temp_directory / x, expect))))
596
+ self.assertSetEqual(
597
+ set(map(lambda x: make_path(x, normalize=True), scan_files(temp_directory / dst))),
598
+ set(map(lambda x: make_path(x, normalize=True), map(lambda x: temp_directory / x, expect))),
599
+ )
@@ -4,7 +4,7 @@ from typing import Dict, List, Optional, SupportsFloat, SupportsInt, Union
4
4
 
5
5
  import ddt
6
6
 
7
- from iker.common.utils.typeutils import is_identical_type, is_optional_type
7
+ from iker.common.utils.typeutils import is_identical_type, is_optional_type, print_type
8
8
 
9
9
 
10
10
  class DummyClass(object):
@@ -309,6 +309,8 @@ class TypeUtilsTest(unittest.TestCase):
309
309
  @ddt.idata(data_is_identical_type)
310
310
  @ddt.unpack
311
311
  def test_is_identical_type(self, a, b, strict_optional, expect):
312
+ print_type(a)
313
+ print_type(b)
312
314
  self.assertEqual(is_identical_type(a, b, strict_optional=strict_optional), expect)
313
315
  self.assertEqual(is_identical_type(b, a, strict_optional=strict_optional), expect)
314
316
 
@@ -407,6 +409,8 @@ class TypeUtilsTest(unittest.TestCase):
407
409
  @ddt.idata(data_is_identical_type__variant)
408
410
  @ddt.unpack
409
411
  def test_is_identical_type__variant(self, a, b, covariant, contravariant, expect):
412
+ print_type(a)
413
+ print_type(b)
410
414
  self.assertEqual(is_identical_type(a,
411
415
  b,
412
416
  strict_optional=False,
@@ -1,239 +0,0 @@
1
- import fnmatch
2
- import os
3
- import shutil
4
- from typing import Protocol
5
-
6
- from iker.common.utils.iterutils import last, last_or_none, tail_iter
7
- from iker.common.utils.strutils import is_empty
8
-
9
- __all__ = [
10
- "extension",
11
- "extensions",
12
- "stem",
13
- "expanded_path",
14
- "norm_path",
15
- "path_depth",
16
- "glob_match",
17
- "listfile",
18
- "copy",
19
- "run",
20
- "execute",
21
- ]
22
-
23
-
24
- def extension(filename: str | os.PathLike[str]) -> str:
25
- """
26
- Extracts the filename extension from the given filename or path.
27
-
28
- :param filename: The specific filename or path.
29
- :return: The filename extension, including the leading dot (e.g., ".txt").
30
- """
31
- _, result = os.path.splitext(os.path.basename(filename))
32
- return result
33
-
34
-
35
- def stem(filename: str | os.PathLike[str], minimal: bool = False) -> str:
36
- """
37
- Extracts the filename stem from the given filename or path.
38
-
39
- :param filename: The specific filename or path.
40
- :param minimal: If ``True``, extracts the minimal (shortest) stem, otherwise the default stem.
41
- :return: The filename stem.
42
- """
43
- base = os.path.basename(filename)
44
- if not minimal:
45
- result, _ = os.path.splitext(base)
46
- return result
47
- else:
48
- maximal_extension = last_or_none(extensions(base))
49
- if is_empty(maximal_extension):
50
- return base
51
- else:
52
- return base[:-len(maximal_extension)]
53
-
54
-
55
- def extensions(filename: str | os.PathLike[str]) -> list[str]:
56
- """
57
- Extracts all filename extensions and compound extensions from the given filename or path.
58
-
59
- :param filename: The specific filename or path.
60
- :return: List of all extensions, ordered from shortest to longest, each including the leading dot.
61
- """
62
- base = os.path.basename(filename)
63
- results = [""]
64
- while True:
65
- fn, ext = os.path.splitext(base)
66
- base = fn
67
- if is_empty(ext):
68
- break
69
- results.append(ext + last(results))
70
- return list(tail_iter(results))
71
-
72
-
73
- def expanded_path(path: str | os.PathLike[str]) -> str:
74
- """
75
- Returns the absolute expanded path, expanding environment variables and the home tilde.
76
-
77
- :param path: The given path.
78
- :return: The absolute canonical path.
79
- """
80
- return os.path.abspath(os.path.expanduser(os.path.expandvars(path)))
81
-
82
-
83
- def norm_path(path: str | os.PathLike[str]) -> str:
84
- """
85
- Returns the normalized path, collapsing redundant separators and up-level references.
86
-
87
- :param path: The given path.
88
- :return: The normalized path.
89
- """
90
- _, path = os.path.splitdrive(os.path.normpath(path))
91
- return path
92
-
93
-
94
- def path_depth(root: str | os.PathLike[str], child: str | os.PathLike[str]) -> int:
95
- """
96
- Returns the relative path depth from the given ``child`` to the ``root``.
97
-
98
- :param root: The root path.
99
- :param child: The child path.
100
- :return: Relative depth, or -1 if ``child`` is not under ``root``.
101
- """
102
- root_expanded = expanded_path(root)
103
- child_expanded = expanded_path(child)
104
- if not child_expanded.startswith(root_expanded):
105
- return -1
106
- return child_expanded[len(root_expanded):].count(os.sep)
107
-
108
-
109
- def glob_match(
110
- names: list[str],
111
- include_patterns: list[str] | None = None,
112
- exclude_patterns: list[str] | None = None,
113
- ) -> list[str]:
114
- """
115
- Applies the given inclusive and exclusive glob patterns to the given ``names`` and returns the filtered result.
116
-
117
- :param names: Names to apply the glob patterns to.
118
- :param include_patterns: Inclusive glob patterns.
119
- :param exclude_patterns: Exclusive glob patterns.
120
- :return: Filtered names matching the patterns.
121
- """
122
- ret = set()
123
- for pat in (include_patterns or []):
124
- ret.update(fnmatch.filter(names, pat))
125
- if include_patterns is None or len(include_patterns) == 0:
126
- ret.update(names)
127
- for pat in (exclude_patterns or []):
128
- ret.difference_update(fnmatch.filter(names, pat))
129
- return list(ret)
130
-
131
-
132
- class CopyFuncProtocol(Protocol):
133
- def __call__(self, src: str | os.PathLike[str], dst: str | os.PathLike[str], **kwargs) -> None: ...
134
-
135
-
136
- def listfile(
137
- path: str | os.PathLike[str],
138
- *,
139
- include_patterns: list[str] | None = None,
140
- exclude_patterns: list[str] | None = None,
141
- depth: int = 0,
142
- ) -> list[str]:
143
- """
144
- Recursively scans the given ``path`` and returns a list of files whose names satisfy the given name patterns and the
145
- relative depth of their folders to the given root path is not greater than the specified ``depth`` value.
146
-
147
- :param path: The root path to scan.
148
- :param include_patterns: Inclusive glob patterns applied to the filenames.
149
- :param exclude_patterns: Exclusive glob patterns applied to the filenames.
150
- :param depth: Maximum depth of the subdirectories included in the scan.
151
- :return: List of file paths matching the criteria.
152
- """
153
- if os.path.exists(path) and not os.path.isdir(path):
154
- if len(glob_match([os.path.basename(path)], include_patterns, exclude_patterns)) == 0:
155
- return []
156
- return [path]
157
-
158
- ret = []
159
- for parent, dirs, filenames in os.walk(path):
160
- if 0 < depth <= path_depth(path, parent):
161
- continue
162
- for filename in glob_match(filenames, include_patterns, exclude_patterns):
163
- ret.append(os.path.join(parent, filename))
164
- return ret
165
-
166
-
167
- def copy(
168
- src: str | os.PathLike[str],
169
- dst: str | os.PathLike[str],
170
- *,
171
- include_patterns: list[str] | None = None,
172
- exclude_patterns: list[str] | None = None,
173
- depth: int = 0,
174
- follow_symlinks: bool = False,
175
- ignore_dangling_symlinks: bool = False,
176
- dirs_exist_ok: bool = False,
177
- copy_func: CopyFuncProtocol = shutil.copy2
178
- ):
179
- """
180
- Recursively copies the given source path to the destination path. Only copies the files whose names satisfy the
181
- given name patterns and the relative depth of their folders to the given source path is not greater than the
182
- specified ``depth`` value.
183
-
184
- :param src: The source path or file.
185
- :param dst: The destination path or file.
186
- :param include_patterns: Inclusive glob patterns applied to the filenames.
187
- :param exclude_patterns: Exclusive glob patterns applied to the filenames.
188
- :param depth: Maximum depth of the subdirectories included in the scan.
189
- :param follow_symlinks: If ``True``, create symbolic links for the symbolic links present in the source; otherwise, make a physical copy.
190
- :param ignore_dangling_symlinks: If ``True``, ignore errors if the file pointed by the symbolic link does not exist.
191
- :param dirs_exist_ok: If ``True``, ignore errors if the destination directory and subdirectories exist.
192
- :param copy_func: Copy function to use for copying files.
193
- """
194
- if not os.path.isdir(src):
195
- if len(glob_match([os.path.basename(src)], include_patterns, exclude_patterns)) == 0:
196
- return
197
- if not os.path.exists(dst):
198
- os.makedirs(os.path.dirname(dst), exist_ok=True)
199
- copy_func(src, dst, follow_symlinks=follow_symlinks)
200
- return
201
-
202
- def ignore_func(parent, names):
203
- filenames = list(filter(lambda x: not os.path.isdir(os.path.join(parent, x)), names))
204
- ret = set(filenames)
205
- if 0 < depth <= path_depth(src, parent):
206
- return ret
207
- ret.difference_update(glob_match(filenames, include_patterns, exclude_patterns))
208
- return ret
209
-
210
- shutil.copytree(src,
211
- dst,
212
- symlinks=follow_symlinks,
213
- ignore=ignore_func,
214
- ignore_dangling_symlinks=ignore_dangling_symlinks,
215
- dirs_exist_ok=dirs_exist_ok,
216
- copy_function=copy_func)
217
-
218
-
219
- def run(cmd: str) -> bool:
220
- """
221
- Runs the given command and returns the success status.
222
-
223
- :param cmd: Command to run.
224
- :return: ``True`` if the command has been successfully run, ``False`` otherwise.
225
- """
226
- return os.system(cmd) == 0
227
-
228
-
229
- def execute(cmd: str, strip: bool = True) -> str:
230
- """
231
- Executes the given command and returns contents collected from standard output.
232
-
233
- :param cmd: Command to execute.
234
- :param strip: If ``True``, the contents will be stripped.
235
- :return: The content from standard output.
236
- """
237
- if strip:
238
- return os.popen(cmd).read().strip()
239
- return os.popen(cmd).read()