1
"""
2
Useful function for managing Input/Output
3

4
Copyright (C) 2017-2019 Jiri Borovec <jiri.borovec@fel.cvut.cz>
5
"""
6

7 30
import logging
8 30
import os
9 30
import warnings
10 30
from functools import wraps
11

12 30
import SimpleITK as sitk
13 30
import cv2 as cv
14 30
import nibabel
15 30
import numpy as np
16 30
import pandas as pd
17 30
import yaml
18 30
from PIL import Image
19 30
from skimage.color import rgb2gray, gray2rgb
20

21
#: landmarks coordinates, loading from CSV file
22 30
LANDMARK_COORDS = ['X', 'Y']
23
# PIL.Image.DecompressionBombError: could be decompression bomb DOS attack.
24
# SEE: https://gitlab.mister-muffin.de/josch/img2pdf/issues/42
25 30
Image.MAX_IMAGE_PIXELS = None
26

27

28 30
def create_folder(path_folder, ok_existing=True):
29
    """ create a folder if it not exists
30

31
    :param str path_folder: path to creating folder
32
    :param bool ok_existing: suppress warning for missing
33
    :return str|None: path to created folder
34

35
    >>> p_dir = create_folder('./sample-folder', ok_existing=True)
36
    >>> create_folder('./sample-folder', ok_existing=False)
37
    False
38
    >>> os.rmdir(p_dir)
39
    """
40 30
    path_folder = os.path.abspath(path_folder)
41 30
    if not os.path.isdir(path_folder):
42 30
        try:
43 30
            os.makedirs(path_folder, mode=0o775)
44 0
        except Exception:
45 0
            logging.exception(
46
                'Something went wrong (probably parallel access), the status of "%s" is %s',
47
                path_folder,
48
                os.path.isdir(path_folder),
49
            )
50 0
            path_folder = None
51 30
    elif not ok_existing:
52 30
        logging.warning('Folder already exists: %s', path_folder)
53 30
        path_folder = False
54

55 30
    return path_folder
56

57

58 30
def load_landmarks(path_file):
59
    """ load landmarks in csv and txt format
60

61
    :param str path_file: path to the input file
62
    :return ndarray: np.array<np_points, dim> of landmarks points
63

64
    >>> points = np.array([[1, 2], [3, 4], [5, 6]])
65
    >>> save_landmarks('./sample_landmarks.csv', points)
66
    >>> pts1 = load_landmarks('./sample_landmarks.csv')
67
    >>> pts2 = load_landmarks('./sample_landmarks.pts')
68
    >>> np.array_equal(pts1, pts2)
69
    True
70
    >>> os.remove('./sample_landmarks.csv')
71
    >>> os.remove('./sample_landmarks.pts')
72

73
    >>> # Wrong loading
74
    >>> load_landmarks('./sample_landmarks.file')
75
    >>> open('./sample_landmarks.file', 'w').close()
76
    >>> load_landmarks('./sample_landmarks.file')
77
    >>> os.remove('./sample_landmarks.file')
78
    """
79 30
    if not os.path.isfile(path_file):
80 30
        logging.warning('missing landmarks "%s"', path_file)
81 30
        return None
82 30
    _, ext = os.path.splitext(path_file)
83 30
    if ext == '.csv':
84 30
        return load_landmarks_csv(path_file)
85 30
    elif ext == '.pts':
86 30
        return load_landmarks_pts(path_file)
87
    else:
88 30
        logging.error('not supported landmarks file: %s', os.path.basename(path_file))
89

90

91 30
def load_landmarks_pts(path_file):
92
    """ load file with landmarks in txt format
93

94
    :param str path_file: path to the input file
95
    :return ndarray: np.array<np_points, dim> of landmarks points
96

97
    >>> points = np.array([[1, 2], [3, 4], [5, 6]])
98
    >>> p_lnds = save_landmarks_pts('./sample_landmarks.csv', points)
99
    >>> p_lnds
100
    './sample_landmarks.pts'
101
    >>> pts = load_landmarks_pts(p_lnds)
102
    >>> pts  # doctest: +NORMALIZE_WHITESPACE
103
    array([[ 1.,  2.],
104
           [ 3.,  4.],
105
           [ 5.,  6.]])
106
    >>> os.remove('./sample_landmarks.pts')
107

108
    >>> # Empty landmarks
109
    >>> open('./sample_landmarks.pts', 'w').close()
110
    >>> load_landmarks_pts('./sample_landmarks.pts').size
111
    0
112
    >>> os.remove('./sample_landmarks.pts')
113
    """
114 30
    assert os.path.isfile(path_file), 'missing file "%s"' % path_file
115 30
    with open(path_file, 'r') as fp:
116 30
        data = fp.read()
117 30
        lines = data.split('\n')
118
        # lines = [re.sub("(\\r|)\\n$", '', line) for line in lines]
119 30
    if len(lines) < 2:
120 30
        logging.warning('invalid format: file has less then 2 lines, "%r"', lines)
121 30
        return np.zeros((0, 2))
122 30
    nb_points = int(lines[1])
123 30
    points = [[float(n) for n in line.split()] for line in lines[2:] if line]
124 30
    assert nb_points == len(points), 'number of declared (%i) and found (%i) ' \
125
                                     'does not match' % (nb_points, len(points))
126 30
    return np.array(points, dtype=np.float)
127

128

129 30
def load_landmarks_csv(path_file):
130
    """ load file with landmarks in cdv format
131

132
    :param str path_file: path to the input file
133
    :return ndarray: np.array<np_points, dim> of landmarks points
134

135
    >>> points = np.array([[1, 2], [3, 4], [5, 6]])
136
    >>> p_lnds = save_landmarks_csv('./sample_landmarks', points)
137
    >>> p_lnds
138
    './sample_landmarks.csv'
139
    >>> pts = load_landmarks_csv(p_lnds)
140
    >>> pts  # doctest: +NORMALIZE_WHITESPACE +ELLIPSIS
141
    array([[1, 2],
142
           [3, 4],
143
           [5, 6]]...)
144
    >>> os.remove('./sample_landmarks.csv')
145
    """
146 30
    assert os.path.isfile(path_file), 'missing file "%s"' % path_file
147 30
    df = pd.read_csv(path_file, index_col=0)
148 30
    points = df[LANDMARK_COORDS].values
149 30
    return points
150

151

152 30
def save_landmarks(path_file, landmarks):
153
    """ save landmarks into a specific file
154

155
    both used formats csv/pts is using the same coordinate frame,
156
    the origin (0, 0) is located in top left corner of the image
157

158
    :param str path_file: path to the output file
159
    :param landmarks: np.array<np_points, dim>
160
    """
161 30
    assert os.path.isdir(os.path.dirname(path_file)), \
162
        'missing folder "%s"' % os.path.dirname(path_file)
163 30
    path_file, _ = os.path.splitext(path_file)
164 30
    landmarks = landmarks.values if isinstance(landmarks, pd.DataFrame) else landmarks
165 30
    save_landmarks_csv(path_file + '.csv', landmarks)
166 30
    save_landmarks_pts(path_file + '.pts', landmarks)
167

168

169 30
def save_landmarks_pts(path_file, landmarks):
170
    """ save landmarks into a txt file
171

172
    we are using VTK pointdata legacy format, ITK compatible::
173

174
        <index, point>
175
        <number of points>
176
        point1-x point1-y [point1-z]
177
        point2-x point2-y [point2-z]
178

179
    .. seealso:: https://simpleelastix.readthedocs.io/PointBasedRegistration.html
180

181
    :param str path_file: path to the output file
182
    :param landmarks: np.array<np_points, dim>
183
    :return str: file path
184
    """
185 30
    assert os.path.isdir(os.path.dirname(path_file)), \
186
        'missing folder "%s"' % os.path.dirname(path_file)
187 30
    path_file = os.path.splitext(path_file)[0] + '.pts'
188 30
    lines = ['point', str(len(landmarks))]
189 30
    lines += [' '.join(str(i) for i in point) for point in landmarks]
190 30
    with open(path_file, 'w') as fp:
191 30
        fp.write('\n'.join(lines))
192 30
    return path_file
193

194

195 30
def save_landmarks_csv(path_file, landmarks):
196
    """ save landmarks into a csv file
197

198
    we are using simple format::
199

200
        ,X,Y
201
        0,point1-x,point1-y
202
        1,point2-x,point2-y
203

204
    :param str path_file: path to the output file
205
    :param landmarks: np.array<np_points, dim>
206
    :return str: file path
207
    """
208 30
    assert os.path.isdir(os.path.dirname(path_file)), \
209
        'missing folder "%s"' % os.path.dirname(path_file)
210 30
    path_file = os.path.splitext(path_file)[0] + '.csv'
211 30
    df = pd.DataFrame(landmarks, columns=LANDMARK_COORDS)
212 30
    df.index = np.arange(1, len(df) + 1)
213 30
    df.to_csv(path_file)
214 30
    return path_file
215

216

217 30
def update_path(a_path, pre_path=None, lim_depth=5, absolute=True):
218
    """ bubble in the folder tree up until it found desired file
219
    otherwise return original one
220

221
    :param str a_path: original path
222
    :param str|None base_path: special case when you want to add something before
223
    :param int lim_depth: max depth of going up in the folder tree
224
    :param bool absolute: format as absolute path
225
    :return str: updated path if it exists otherwise the original one
226

227
    >>> os.path.exists(update_path('./birl', absolute=False))
228
    True
229
    >>> os.path.exists(update_path('/', absolute=False))
230
    True
231
    >>> os.path.exists(update_path('~', absolute=False))
232
    True
233
    """
234 30
    path_ = str(a_path)
235 30
    if path_.startswith('/'):
236 30
        return path_
237 30
    elif path_.startswith('~'):
238 30
        path_ = os.path.expanduser(path_)
239
    # special case when you want to add something before
240 30
    elif pre_path:
241 30
        path_ = os.path.join(pre_path, path_)
242

243 30
    tmp_path = path_[2:] if path_.startswith('./') else path_
244 30
    for _ in range(lim_depth):
245 30
        if os.path.exists(tmp_path):
246 30
            path_ = tmp_path
247 30
            break
248 30
        tmp_path = os.path.join('..', tmp_path)
249

250 30
    if absolute:
251 30
        path_ = os.path.abspath(path_)
252 30
    return path_
253

254

255 30
def io_image_decorate(func):
256
    """ costume decorator to suppers debug messages from the PIL function
257
    to suppress PIl debug logging
258
    - DEBUG:PIL.PngImagePlugin:STREAM b'IHDR' 16 13
259

260
    :param func: decorated function
261
    :return func: output of the decor. function
262
    """
263

264 30
    @wraps(func)
265 6
    def wrap(*args, **kwargs):
266 30
        log_level = logging.getLogger().getEffectiveLevel()
267 30
        logging.getLogger().setLevel(logging.INFO)
268 30
        with warnings.catch_warnings():
269 30
            warnings.simplefilter("ignore")
270 30
            response = func(*args, **kwargs)
271 30
        logging.getLogger().setLevel(log_level)
272 30
        return response
273

274 30
    return wrap
275

276

277 30
@io_image_decorate
278 30
def image_sizes(path_image, decimal=1):
279
    """ get image size (without loading image raster)
280

281
    :param str path_image: path to the image
282
    :param int decimal: rounding digits
283
    :return tuple(tuple(int,int),float): image size (height, width) and diagonal
284

285
    >>> img = np.random.random((50, 75, 3))
286
    >>> save_image('./test_image.jpg', img)
287
    >>> image_sizes('./test_image.jpg', decimal=0)
288
    ((50, 75), 90.0)
289
    >>> os.remove('./test_image.jpg')
290
    """
291 30
    assert os.path.isfile(path_image), 'missing image: %s' % path_image
292 30
    width, height = Image.open(path_image).size
293 30
    img_diag = np.sqrt(np.sum(np.array([height, width])**2))
294 30
    return (height, width), np.round(img_diag, decimal)
295

296

297 30
@io_image_decorate
298 30
def load_image(path_image, force_rgb=True):
299
    """ load the image in value range (0, 1)
300

301
    :param str path_image: path to the image
302
    :param bool force_rgb: convert RGB image
303
    :return ndarray: np.array<height, width, ch>
304

305
    >>> img = np.random.random((50, 50))
306
    >>> save_image('./test_image.jpg', img)
307
    >>> img2 = load_image('./test_image.jpg')
308
    >>> img2.max() <= 1.
309
    True
310
    >>> os.remove('./test_image.jpg')
311
    """
312 30
    assert os.path.isfile(path_image), 'missing image "%s"' % path_image
313 30
    image = np.array(Image.open(path_image))
314 30
    while image.max() > 1.5:
315 30
        image = image / 255.
316 30
    if force_rgb and (image.ndim == 2 or image.shape[2] == 1):
317 30
        image = image[:, :, 0] if image.ndim == 3 else image
318 30
        image = gray2rgb(image)
319 30
    return image.astype(np.float32)
320

321

322 30
def convert_ndarray2image(image):
323
    """ convert ndarray to PIL image if it not already
324

325
    :param ndarray image: input image
326
    :return Image: output image
327

328
    >>> img = np.random.random((50, 50, 3))
329
    >>> image = convert_ndarray2image(img)
330
    >>> isinstance(image, Image.Image)
331
    True
332
    """
333 30
    if isinstance(image, np.ndarray):
334 30
        if np.max(image) <= 1.5:
335 30
            image = image * 255
336 30
        np.clip(image, a_min=0, a_max=255, out=image)
337 30
        if image.ndim == 3 and image.shape[-1] < 3:
338 0
            image = image[:, :, 0]
339 30
        image = Image.fromarray(image.astype(np.uint8))
340 30
    return image
341

342

343 30
@io_image_decorate
344 6
def save_image(path_image, image):
345
    """ save the image into given path
346

347
    :param str path_image: path to the image
348
    :param image: np.array<height, width, ch>
349

350
    >>> # Wrong path
351
    >>> save_image('./missing-path/any-image.png', np.zeros((10, 20)))
352
    False
353
    """
354 30
    path_dir = os.path.dirname(path_image)
355 30
    if not os.path.isdir(path_dir):
356 30
        logging.error('upper folder does not exists: "%s"', path_dir)
357 30
        return False
358 30
    image = convert_ndarray2image(image)
359 30
    image.save(path_image)
360

361

362 30
@io_image_decorate
363 30
def convert_image_to_nifti(path_image, path_out_dir=None):
364
    """ converting normal image to Nifty Image
365

366
    :param str path_image: input image
367
    :param str path_out_dir: path to output folder
368
    :return str: resulted image
369

370
    >>> path_img = os.path.join(update_path('data-images'), 'images',
371
    ...                         'artificial_moving-affine.jpg')
372
    >>> path_img2 = convert_image_to_nifti(path_img, '.')
373
    >>> path_img2  # doctest: +ELLIPSIS
374
    '...artificial_moving-affine.nii'
375
    >>> os.path.isfile(path_img2)
376
    True
377
    >>> path_img3 = convert_image_from_nifti(path_img2)
378
    >>> os.path.isfile(path_img3)
379
    True
380
    >>> list(map(os.remove, [path_img2, path_img3]))  # doctest: +ELLIPSIS
381
    [...]
382
    """
383 30
    path_image = update_path(path_image)
384 30
    path_img_out = _gene_out_path(path_image, '.nii', path_out_dir)
385 30
    logging.debug('Convert image to Nifti format "%s" ->  "%s"', path_image, path_img_out)
386

387
    # img = Image.open(path_file).convert('LA')
388 30
    img = load_image(path_image)
389 30
    nim = nibabel.Nifti1Pair(img, np.eye(4))
390 30
    del img
391 30
    nibabel.save(nim, path_img_out)
392

393 30
    return path_img_out
394

395

396 30
@io_image_decorate
397 30
def convert_image_to_nifti_gray(path_image, path_out_dir=None):
398
    """ converting normal image to Nifty Image
399

400
    :param str path_image: input image
401
    :param str path_out_dir: path to output folder
402
    :return str: resulted image
403

404
    >>> path_img = './sample-image.png'
405
    >>> save_image(path_img, np.zeros((100, 200, 3)))
406
    >>> path_img2 = convert_image_to_nifti_gray(path_img)
407
    >>> os.path.isfile(path_img2)
408
    True
409
    >>> path_img3 = convert_image_from_nifti(path_img2, '.')
410
    >>> os.path.isfile(path_img3)
411
    True
412
    >>> list(map(os.remove, [path_img, path_img2, path_img3]))  # doctest: +ELLIPSIS
413
    [...]
414
    """
415 30
    path_image = update_path(path_image)
416 30
    path_img_out = _gene_out_path(path_image, '.nii', path_out_dir)
417 30
    logging.debug('Convert image to Nifti format "%s" ->  "%s"', path_image, path_img_out)
418

419
    # img = Image.open(path_file).convert('LA')
420 30
    img = rgb2gray(load_image(path_image))
421 30
    nim = nibabel.Nifti1Pair(np.swapaxes(img, 1, 0), np.eye(4))
422 30
    del img
423 30
    nibabel.save(nim, path_img_out)
424

425 30
    return path_img_out
426

427

428 30
def _gene_out_path(path_file, file_ext, path_out_dir=None):
429
    """ generate new path with the same file name just changed extension (and folder)
430

431
    :param str path_file: path of source file (image)
432
    :param str file_ext: file extension of desired file
433
    :param str path_out_dir: destination folder, if NOne use the input dir
434
    :return str: desired file
435
    """
436 30
    if not path_out_dir:
437 30
        path_out_dir = os.path.dirname(path_file)
438 30
    img_name, _ = os.path.splitext(os.path.basename(path_file))
439 30
    path_out = os.path.join(path_out_dir, img_name + file_ext)
440 30
    return path_out
441

442

443 30
@io_image_decorate
444 30
def convert_image_from_nifti(path_image, path_out_dir=None):
445
    """ converting Nifti to standard image
446

447
    :param str path_image: path to input image
448
    :param str path_out_dir: destination directory, if Nne use the same folder
449
    :return str: path to new image
450
    """
451 30
    path_image = update_path(path_image)
452 30
    path_img_out = _gene_out_path(path_image, '.jpg', path_out_dir)
453 30
    logging.debug('Convert Nifti to image format "%s" ->  "%s"', path_image, path_img_out)
454 30
    nim = nibabel.load(path_image)
455

456 30
    if len(nim.get_data().shape) > 2:  # colour
457 30
        img = nim.get_data()
458
    else:  # gray
459 30
        img = np.swapaxes(nim.get_data(), 1, 0)
460

461 30
    if img.max() > 1.5:
462 0
        img = img / 255.
463

464 30
    save_image(path_img_out, img)
465 30
    return path_img_out
466

467

468 30
def image_resize(img, scale=1., v_range=255, dtype=int):
469
    """ rescale image with other optional formating
470

471
    :param ndarray img: input image
472
    :param float scale: the new image size is im_size * scale
473
    :param int|float v_range: desired output image range 1. or 255
474
    :param dtype: output image type
475
    :return ndarray: image
476

477
    >>> np.random.seed(0)
478
    >>> img = image_resize(np.random.random((250, 300, 3)), scale=2, v_range=255)
479
    >>> np.array(img.shape, dtype=int)
480
    array([500, 600,   3])
481
    >>> img.max()
482
    255
483
    """
484 30
    if scale is None or scale == 1:
485 0
        return img
486
    # scale the image accordingly
487 30
    interp = cv.INTER_CUBIC if scale > 1 else cv.INTER_LINEAR
488 30
    img = cv.resize(img, None, fx=scale, fy=scale, interpolation=interp)
489

490 30
    v_range = 255 if v_range > 1.5 else 1.
491
    # if resulting image is in range to 1 and desired is in 255
492 30
    if np.max(img) < 1.5 < v_range:
493 30
        np.multiply(img, 255, out=img)
494 30
        np.round(img, out=img)
495

496
    # convert image datatype
497 30
    if dtype is not None:
498 30
        img = img.astype(dtype)
499

500
    # clip image values in certain range
501 30
    np.clip(img, a_min=0, a_max=v_range, out=img)
502 30
    return img
503

504

505 30
@io_image_decorate
506 30
def convert_image_from_mhd(path_image, path_out_dir=None, img_ext='.png', scaling=None):
507
    """ convert standard image to MHD format
508

509
    .. seealso:: https://www.programcreek.com/python/example/96382/SimpleITK.WriteImage
510

511
    :param str path_image: path to the input image
512
    :param str path_out_dir: path to output directory, if None use the input dir
513
    :param str img_ext: image extension like PNG or JPEG
514
    :param float|None scaling: image down-scaling,
515
        resulting image will be larger by this factor
516
    :return str: path to exported image
517

518
    >>> path_img = os.path.join(update_path('data-images'), 'images',
519
    ...                         'artificial_reference.jpg')
520
    >>> path_img = convert_image_to_mhd(path_img, scaling=1.5)
521
    >>> convert_image_from_mhd(path_img, scaling=1.5)  # doctest: +ELLIPSIS
522
    '...artificial_reference.png'
523
    """
524 30
    path_image = update_path(path_image)
525 30
    assert os.path.isfile(path_image), 'missing image: %s' % path_image
526
    # Reads the image using SimpleITK
527 30
    itk_image = sitk.ReadImage(path_image)
528

529
    # Convert the image to a numpy array first and then shuffle the dimensions
530
    # to get axis in the order z,y,x
531 30
    img = sitk.GetArrayFromImage(itk_image)
532
    # Scaling image if requested
533 30
    img = image_resize(img, scaling, v_range=255)
534

535
    # define output/destination path
536 30
    path_image = _gene_out_path(path_image, img_ext, path_out_dir)
537 30
    save_image(path_image, img)
538 30
    return path_image
539

540

541 30
@io_image_decorate
542 30
def convert_image_to_mhd(path_image, path_out_dir=None, to_gray=True, overwrite=True, scaling=None):
543
    """ converting standard image to MHD (Nifty format)
544

545
    .. seealso:: https://stackoverflow.com/questions/37290631
546

547
    :param str path_image: path to the input image
548
    :param str path_out_dir: path to output directory, if None use the input dir
549
    :param bool overwrite: allow overwrite existing image
550
    :param float|None scaling: image up-scaling
551
        resulting image will be smaller by this factor
552
    :return str: path to exported image
553

554
    >>> path_img = os.path.join(update_path('data-images'), 'images',
555
    ...                         'artificial_moving-affine.jpg')
556
    >>> convert_image_to_mhd(path_img, scaling=2)  # doctest: +ELLIPSIS
557
    '...artificial_moving-affine.mhd'
558
    """
559 30
    path_image = update_path(path_image)
560 30
    path_image_new = _gene_out_path(path_image, '.mhd', path_out_dir)
561
    # in case the image exists and you are not allowed to overwrite it
562 30
    if os.path.isfile(path_image_new) and not overwrite:
563 0
        logging.debug('skip converting since the image exists and no-overwrite: %s', path_image_new)
564 0
        return path_image_new
565

566 30
    img = load_image(path_image)
567
    # if required and RGB on input convert to gray-scale
568 30
    if to_gray and img.ndim == 3 and img.shape[2] in (3, 4):
569 30
        img = rgb2gray(img)
570
    # Scaling image if requested
571 30
    scaling = 1. / scaling
572
    # the MHD usually require pixel value range (0, 255)
573 30
    img = image_resize(img, scaling, v_range=255)
574

575 30
    logging.debug('exporting image of size: %r', img.shape)
576 30
    image = sitk.GetImageFromArray(img.astype(np.uint8), isVector=False)
577

578
    # do not use text in MHD, othwerwise it crash DROP method
579 30
    sitk.WriteImage(image, path_image_new, False)
580 30
    return path_image_new
581

582

583 30
def load_config_args(path_config, comment='#'):
584
    """load config arguments from file with dropping comments
585

586
    :param str path_config: configuration file
587
    :param str comment: character defining comments
588
    :return str: concat arguments
589

590
    >>> p_conf = './sample-arg-config.txt'
591
    >>> with open(p_conf, 'w') as fp:
592
    ...     fp.writelines(os.linesep.join(['# comment', '', ' -a 1  ', ' --b c#d']))
593
    >>> load_config_args(p_conf)
594
    '-a 1 --b c'
595
    >>> os.remove(p_conf)
596
    """
597 30
    assert os.path.isfile(path_config), 'missing file: %s' % path_config
598 30
    lines = []
599 30
    with open(path_config, 'r') as fp:
600 30
        for ln in fp.readlines():
601
            # drop comments
602 30
            if comment in ln:
603 30
                ln = ln[:ln.index(comment)]
604
            # remove spaces from beinning and end
605 30
            ln = ln.strip()
606
            # skip empty lines
607 30
            if ln:
608 30
                lines.append(ln)
609 30
    config = ' '.join(lines)
610 30
    return config
611

612

613 30
def load_config_yaml(path_config):
614
    """ loading the
615

616
    :param str path_config:
617
    :return dict:
618

619
    >>> p_conf = './testing-congif.yaml'
620
    >>> save_config_yaml(p_conf, {'a': 2})
621
    >>> load_config_yaml(p_conf)
622
    {'a': 2}
623
    >>> os.remove(p_conf)
624
    """
625 30
    with open(path_config, 'r') as fp:
626 30
        config = yaml.safe_load(fp)
627 30
    return config
628

629

630 30
def save_config_yaml(path_config, config):
631
    """ exporting configuration as YAML file
632

633
    :param str path_config:
634
    :param dict config:
635
    """
636 30
    with open(path_config, 'w') as fp:
637 30
        yaml.dump(config, fp, default_flow_style=False)

Read our documentation on viewing source code .

Loading