Completed
Push — master ( 6ef2f5...b75cb1 )
by Satoru
53s
created

is_path_like_object()   B

Complexity

Conditions 1

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 5
Bugs 1 Features 0
Metric Value
cc 1
c 5
b 1
f 0
dl 0
loc 26
rs 8.8571
1
#
2
# Copyright (C) 2012 - 2018 Satoru SATOH <ssato @ redhat.com>
3
# License: MIT
4
#
5
"""Misc utility routines for anyconfig module.
6
"""
7
from __future__ import absolute_import
8
9
import collections
10
import glob
11
import os.path
12
import types
13
14
import anyconfig.compat
15
import anyconfig.globals
16
17
from anyconfig.compat import pathlib
18
19
20
def get_file_extension(file_path):
21
    """
22
    >>> get_file_extension("/a/b/c")
23
    ''
24
    >>> get_file_extension("/a/b.txt")
25
    'txt'
26
    >>> get_file_extension("/a/b/c.tar.xz")
27
    'xz'
28
    """
29
    _ext = os.path.splitext(file_path)[-1]
30
    if _ext:
31
        return _ext[1:] if _ext.startswith('.') else _ext
32
33
    return ""
34
35
36
def sglob(files_pattern):
37
    """
38
    glob.glob alternative of which results sorted always.
39
    """
40
    return sorted(glob.glob(files_pattern))
41
42
43
def is_iterable(obj):
44
    """
45
    >>> is_iterable([])
46
    True
47
    >>> is_iterable(())
48
    True
49
    >>> is_iterable([x for x in range(10)])
50
    True
51
    >>> is_iterable((1, 2, 3))
52
    True
53
    >>> g = (x for x in range(10))
54
    >>> is_iterable(g)
55
    True
56
    >>> is_iterable("abc")
57
    False
58
    >>> is_iterable(0)
59
    False
60
    >>> is_iterable({})
61
    False
62
    """
63
    return isinstance(obj, (list, tuple, types.GeneratorType)) or \
64
        (not isinstance(obj, (int, str, dict)) and
65
         bool(getattr(obj, "next", False)))
66
67
68
def concat(xss):
69
    """
70
    Concatenates a list of lists.
71
72
    >>> concat([[]])
73
    []
74
    >>> concat((()))
75
    []
76
    >>> concat([[1,2,3],[4,5]])
77
    [1, 2, 3, 4, 5]
78
    >>> concat([[1,2,3],[4,5,[6,7]]])
79
    [1, 2, 3, 4, 5, [6, 7]]
80
    >>> concat(((1,2,3),(4,5,[6,7])))
81
    [1, 2, 3, 4, 5, [6, 7]]
82
    >>> concat(((1,2,3),(4,5,[6,7])))
83
    [1, 2, 3, 4, 5, [6, 7]]
84
    >>> concat((i, i*2) for i in range(3))
85
    [0, 0, 1, 2, 2, 4]
86
    """
87
    return list(anyconfig.compat.from_iterable(xs for xs in xss))
88
89
90
def normpath(path):
91
    """Normalize path.
92
93
    - eliminating double slashes, etc. (os.path.normpath)
94
    - ensure paths contain ~[user]/ expanded.
95
96
    :param path: Path string :: str
97
    """
98
    return os.path.normpath(os.path.expanduser(path) if '~' in path else path)
99
100
101
def is_path(obj):
102
    """
103
    Is given object `obj` a file path?
104
105
    :param obj: file path or something
106
    :return: True if `obj` is a file path
107
    """
108
    return isinstance(obj, anyconfig.compat.STR_TYPES)
109
110
111
def is_path_obj(obj):
112
    """Is given object `input` a pathlib.Path object?
113
114
    :param obj: a pathlib.Path object or something
115
    :return: True if `obj` is a pathlib.Path object
116
117
    >>> from anyconfig.compat import pathlib
118
    >>> if pathlib is not None:
119
    ...      obj = pathlib.Path(__file__)
120
    ...      assert is_path_obj(obj)
121
    >>>
122
    >>> assert not is_path_obj(__file__)
123
    """
124
    return pathlib is not None and isinstance(obj, pathlib.Path)
125
126
127
def is_file_stream(obj):
128
    """Is given object `input` a file stream (file/file-like object)?
129
130
    :param obj: a file / file-like (stream) object or something
131
    :return: True if `obj` is a file stream
132
133
    >>> assert is_file_stream(open(__file__))
134
    >>> assert not is_file_stream(__file__)
135
    """
136
    return getattr(obj, "read", False)
137
138
139
def is_ioinfo(obj, keys=None):
140
    """
141
    :return: True if given `obj` is a 'IOInfo' namedtuple object.
142
143
    >>> assert not is_ioinfo(1)
144
    >>> assert not is_ioinfo("aaa")
145
    >>> assert not is_ioinfo({})
146
    >>> assert not is_ioinfo(('a', 1, {}))
147
148
    >>> inp = anyconfig.globals.IOInfo("/etc/hosts", "path", "/etc/hosts",
149
    ...                                None, open)
150
    >>> assert is_ioinfo(inp)
151
    """
152
    if keys is None:
153
        keys = anyconfig.globals.IOI_KEYS
154
155
    if isinstance(obj, tuple) and getattr(obj, "_asdict", False):
156
        return all(k in obj._asdict() for k in keys)
157
158
    return False
159
160
161
def is_stream_ioinfo(obj):
162
    """
163
    :param obj: IOInfo object or something
164
    :return: True if given IOInfo object `obj` is of file / file-like object
165
166
    >>> ioi = anyconfig.globals.IOInfo(None, anyconfig.globals.IOI_STREAM,
167
    ...                                None, None, None)
168
    >>> assert is_stream_ioinfo(ioi)
169
    >>> assert not is_stream_ioinfo(__file__)
170
    """
171
    return getattr(obj, "type", None) == anyconfig.globals.IOI_STREAM
172
173
174
def is_path_like_object(obj, marker='*'):
175
    """
176
    Is given object `obj` a path string, a pathlib.Path, a file / file-like
177
    (stream) or IOInfo namedtuple object?
178
179
    :param obj:
180
        a path string, pathlib.Path object, a file / file-like or 'IOInfo'
181
        object
182
183
    :return:
184
        True if `obj` is a path string or a pathlib.Path object or a file
185
        (stream) object
186
187
    >>> assert is_path_like_object(__file__)
188
    >>> assert not is_path_like_object("/a/b/c/*.json", '*')
189
190
    >>> from anyconfig.compat import pathlib
191
    >>> if pathlib is not None:
192
    ...      assert is_path_like_object(pathlib.Path("a.ini"))
193
    ...      assert not is_path_like_object(pathlib.Path("x.ini"), 'x')
194
195
    >>> assert is_path_like_object(open(__file__))
196
    """
197
    return ((is_path(obj) and marker not in obj) or
198
            (is_path_obj(obj) and marker not in obj.as_posix()) or
199
            is_file_stream(obj) or is_ioinfo(obj))
200
201
202
def is_paths(maybe_paths, marker='*'):
203
    """
204
    Does given object `maybe_paths` consist of path or path pattern strings?
205
    """
206
    return ((is_path(maybe_paths) and marker in maybe_paths) or  # Path str
207
            (is_path_obj(maybe_paths) and marker in maybe_paths.as_posix()) or
208
            (is_iterable(maybe_paths) and
209
             all(is_path(p) or is_ioinfo(p) for p in maybe_paths)))
210
211
212
def get_path_from_stream(strm):
213
    """
214
    Try to get file path from given file or file-like object `strm`.
215
216
    :param strm: A file or file-like object
217
    :return: Path of given file or file-like object or None
218
    :raises: ValueError
219
220
    >>> assert __file__ == get_path_from_stream(open(__file__, 'r'))
221
    >>> assert get_path_from_stream(anyconfig.compat.StringIO()) is None
222
    >>> get_path_from_stream(__file__)  # doctest: +ELLIPSIS
223
    Traceback (most recent call last):
224
        ...
225
    ValueError: ...
226
    """
227
    if not is_file_stream(strm):
228
        raise ValueError("Given object does not look a file/file-like "
229
                         "object: %r" % strm)
230
231
    path = getattr(strm, "name", None)
232
    if path is not None:
233
        return normpath(path)
234
235
    return None
236
237
238
def _try_to_get_extension(obj):
239
    """
240
    Try to get file extension from given path or file object.
241
242
    :param obj: a file, file-like object or something
243
    :return: File extension or None
244
245
    >>> _try_to_get_extension("a.py")
246
    'py'
247
    """
248
    if is_path(obj):
249
        path = obj
250
251
    elif is_path_obj(obj):
252
        return obj.suffix[1:]
253
254
    elif is_file_stream(obj):
255
        try:
256
            path = get_path_from_stream(obj)
257
        except ValueError:
258
            return None
259
260
    elif is_ioinfo(obj):
261
        path = obj.path
262
263
    else:
264
        return None
265
266
    if path:
267
        return get_file_extension(path)
268
269
    return None
270
271
272
def are_same_file_types(objs):
273
    """
274
    Are given (maybe) file objs same type (extension) ?
275
276
    :param objs: A list of file path or file(-like) objects
277
278
    >>> are_same_file_types([])
279
    False
280
    >>> are_same_file_types(["a.conf"])
281
    True
282
    >>> are_same_file_types(["a.conf", "b.conf"])
283
    True
284
    >>> are_same_file_types(["a.yml", "b.yml"])
285
    True
286
    >>> are_same_file_types(["a.yml", "b.json"])
287
    False
288
    >>> strm = anyconfig.compat.StringIO()
289
    >>> are_same_file_types(["a.yml", "b.yml", strm])
290
    False
291
    """
292
    if not objs:
293
        return False
294
295
    ext = _try_to_get_extension(objs[0])
296
    if ext is None:
297
        return False
298
299
    return all(_try_to_get_extension(p) == ext for p in objs[1:])
300
301
302
def _expand_paths_itr(paths, marker='*'):
303
    """Iterator version of :func:`expand_paths`.
304
    """
305
    for path in paths:
306
        if is_path(path):
307
            if marker in path:  # glob path pattern
308
                for ppath in sglob(path):
309
                    yield ppath
310
            else:
311
                yield path  # a simple file path
312
        elif is_path_obj(path):
313
            if marker in path.as_posix():
314
                for ppath in sglob(path.as_posix()):
315
                    yield normpath(ppath)
316
            else:
317
                yield normpath(path.as_posix())
318
        elif is_ioinfo(path):
319
            yield path.path
320
        else:  # A file or file-like object
321
            yield path
322
323
324
def expand_paths(paths, marker='*'):
325
    """
326
    :param paths:
327
        A glob path pattern string or pathlib.Path object holding such path, or
328
        a list consists of path strings or glob path pattern strings or
329
        pathlib.Path object holding such ones, or file objects
330
    :param marker: Glob marker character or string, e.g. '*'
331
332
    :return: List of path strings
333
334
    >>> expand_paths([])
335
    []
336
    >>> expand_paths("/usr/lib/a/b.conf /etc/a/b.conf /run/a/b.conf".split())
337
    ['/usr/lib/a/b.conf', '/etc/a/b.conf', '/run/a/b.conf']
338
    >>> paths_s = os.path.join(os.path.dirname(__file__), "u*.py")
339
    >>> ref = sglob(paths_s)
340
    >>> assert expand_paths(paths_s) == ref
341
    >>> ref = ["/etc/a.conf"] + ref
342
    >>> assert expand_paths(["/etc/a.conf", paths_s]) == ref
343
    >>> strm = anyconfig.compat.StringIO()
344
    >>> assert expand_paths(["/etc/a.conf", strm]) == ["/etc/a.conf", strm]
345
    """
346
    if is_path(paths) and marker in paths:
347
        return sglob(paths)
348
349
    if is_path_obj(paths) and marker in paths.as_posix():
350
        # TBD: Is it better to return [p :: pathlib.Path] instead?
351
        return [normpath(p) for p in sglob(paths.as_posix())]
352
353
    return list(_expand_paths_itr(paths, marker=marker))
354
355
356
# pylint: disable=unused-argument
357
def noop(val, *args, **kwargs):
358
    """A function does nothing.
359
360
    >>> noop(1)
361
    1
362
    """
363
    # It means nothing but can suppress 'Unused argument' pylint warns.
364
    # (val, args, kwargs)[0]
365
    return val
366
367
368
_LIST_LIKE_TYPES = (collections.Iterable, collections.Sequence)
369
370
371
def is_dict_like(obj):
372
    """
373
    :param obj: Any object behaves like a dict.
374
375
    >>> is_dict_like("a string")
376
    False
377
    >>> is_dict_like({})
378
    True
379
    >>> is_dict_like(anyconfig.compat.OrderedDict((('a', 1), ('b', 2))))
380
    True
381
    """
382
    return isinstance(obj, (dict, collections.Mapping))  # any others?
383
384
385
def is_namedtuple(obj):
386
    """
387
    >>> p0 = collections.namedtuple("Point", "x y")(1, 2)
388
    >>> is_namedtuple(p0)
389
    True
390
    >>> is_namedtuple(tuple(p0))
391
    False
392
    """
393
    return isinstance(obj, tuple) and hasattr(obj, "_asdict")
394
395
396
def is_list_like(obj):
397
    """
398
    >>> is_list_like([])
399
    True
400
    >>> is_list_like(())
401
    True
402
    >>> is_list_like([x for x in range(10)])
403
    True
404
    >>> is_list_like((1, 2, 3))
405
    True
406
    >>> g = (x for x in range(10))
407
    >>> is_list_like(g)
408
    True
409
    >>> is_list_like("abc")
410
    False
411
    >>> is_list_like(0)
412
    False
413
    >>> is_list_like({})
414
    False
415
    """
416
    return isinstance(obj, _LIST_LIKE_TYPES) and \
417
        not (isinstance(obj, anyconfig.compat.STR_TYPES) or is_dict_like(obj))
418
419
420
def filter_options(keys, options):
421
    """
422
    Filter `options` with given `keys`.
423
424
    :param keys: key names of optional keyword arguments
425
    :param options: optional keyword arguments to filter with `keys`
426
427
    >>> filter_options(("aaa", ), dict(aaa=1, bbb=2))
428
    {'aaa': 1}
429
    >>> filter_options(("aaa", ), dict(bbb=2))
430
    {}
431
    """
432
    return dict((k, options[k]) for k in keys if k in options)
433
434
# vim:sw=4:ts=4:et:
435