Completed
Push — master ( 152b55...0bdad9 )
by John
01:22
created

Spinner.__init__()   A

Complexity

Conditions 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
#!/usr/bin/env python3
2
"""This module is used for miscellaneous utilities."""
3
4
import os  # path work
5
import argparse  # argument parser for filters
6
import platform  # platform info
7
import glob  # cap grabbing
8
import configparser  # config parsing, duh
9
import threading  # get thread for spinner
10
import time  # spinner delay
11
import sys  # streams, version info
12
import itertools  # spinners gonna spin
13
import subprocess  # loader verification
14
from bbarchivist import bbconstants  # cap location, version, filename bits
15
from bbarchivist import compat  # backwards compat
16
from bbarchivist import dummy  # useless stdout
17
18
__author__ = "Thurask"
19
__license__ = "WTFPL v2"
20
__copyright__ = "Copyright 2015-2016 Thurask"
21
22
23
def grab_cap():
24
    """
25
    Figure out where cap is, local, specified or system-supplied.
26
    """
27
    try:
28
        capfile = glob.glob(os.path.join(os.getcwd(), bbconstants.CAP.filename))[0]
29
    except IndexError:
30
        try:
31
            cappath = cappath_config_loader()
32
            capfile = glob.glob(cappath)[0]
33
        except IndexError:
34
            cappath_config_writer(bbconstants.CAP.location)
35
            return bbconstants.CAP.location  # no ini cap
36
        else:
37
            cappath_config_writer(os.path.abspath(capfile))
38
            return os.path.abspath(capfile)  # ini cap
39
    else:
40
        return os.path.abspath(capfile)  # local cap
41
42
43
def grab_cfp():
44
    """
45
    Figure out where cfp is, local or system-supplied.
46
    """
47
    try:
48
        cfpfile = glob.glob(os.path.join(os.getcwd(), bbconstants.CFP.filename))[0]
49
    except IndexError:
50
        cfpfile = bbconstants.CFP.location  # system cfp
51
    return os.path.abspath(cfpfile)  # local cfp
52
53
54
def new_enough(minver):
55
    """
56
    Check if we're at or above a minimum Python version.
57
58
    :param minver: Minimum Python version (3.minver).
59
    :type minver: int
60
    """
61
    return False if minver > sys.version_info[1] else True
62
63
64
def fsizer(file_size):
65
    """
66
    Raw byte file size to human-readable string.
67
68
    :param file_size: Number to parse.
69
    :type file_size: float
70
    """
71
    if file_size is None:
72
        file_size = 0
73
    fsize = float(file_size)
74
    for sfix in ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB']:
75
        if fsize < 1024.0:
76
            size = "{0:3.2f}{1}".format(fsize, sfix)
77
            break
78
        else:
79
            fsize /= 1024.0
80
    else:
81
        size = "{0:3.2f}{1}".format(fsize, 'YB')
82
    return size
83
84
85
def signed_file_args(files):
86
    """
87
    Check if there are between 1 and 6 files supplied to argparse.
88
89
    :param files: List of signed files, between 1 and 6 strings.
90
    :type files: list(str)
91
    """
92
    filelist = [file for file in files if file]
93
    if not 1 <= len(filelist) <= 6:
94
        raise argparse.ArgumentError(argument=None, message="Requires 1-6 signed files")
95
    return files
96
97
98
def file_exists(file):
99
    """
100
    Check if file exists, raise argparse error if it doesn't.
101
102
    :param file: Path to a file, including extension.
103
    :type file: str
104
    """
105
    if not os.path.exists(file):
106
        raise argparse.ArgumentError(argument=None, message="{0} not found.".format(file))
107
    return file
108
109
110
def positive_integer(input_int):
111
    """
112
    Check if number > 0, raise argparse error if it isn't.
113
114
    :param input_int: Integer to check.
115
    :type input_int: str
116
    """
117
    if int(input_int) <= 0:
118
        info = "{0} is not >=0.".format(str(input_int))
119
        raise argparse.ArgumentError(argument=None, message=info)
120
    return int(input_int)
121
122
123
def valid_method(method):
124
    """
125
    Check if compression method is valid, raise argparse error if it isn't.
126
127
    :param method: Compression method to check.
128
    :type method: str
129
    """
130
    methodlist = bbconstants.METHODS
131
    if not new_enough(3):
132
        methodlist = [x for x in bbconstants.METHODS if x != "txz"]
133
    if method not in methodlist:
134
        info = "Invalid method {0}.".format(method)
135
        raise argparse.ArgumentError(argument=None, message=info)
136
    return method
137
138
139
def valid_carrier(mcc_mnc):
140
    """
141
    Check if MCC/MNC is valid (1-3 chars), raise argparse error if it isn't.
142
143
    :param mcc_mnc: MCC/MNC to check.
144
    :type mcc_mnc: str
145
    """
146
    if not str(mcc_mnc).isdecimal():
147
        infod = "Non-integer {0}.".format(str(mcc_mnc))
148
        raise argparse.ArgumentError(argument=None, message=infod)
149
    if len(str(mcc_mnc)) > 3 or len(str(mcc_mnc)) == 0:
150
        infol = "{0} is invalid.".format(str(mcc_mnc))
151
        raise argparse.ArgumentError(argument=None, message=infol)
152
    else:
153
        return mcc_mnc
154
155
156
def escreens_pin(pin):
157
    """
158
    Check if given PIN is valid, raise argparse error if it isn't.
159
160
    :param pin: PIN to check.
161
    :type pin: str
162
    """
163
    if len(pin) == 8:
164
        try:
165
            int(pin, 16)  # hexadecimal-ness
166
        except ValueError:
167
            raise argparse.ArgumentError(argument=None, message="Invalid PIN.")
168
        else:
169
            return pin.lower()
170
    else:
171
        raise argparse.ArgumentError(argument=None, message="Invalid PIN.")
172
173
174
def escreens_duration(duration):
175
    """
176
    Check if Engineering Screens duration is valid.
177
178
    :param duration: Duration to check.
179
    :type duration: int
180
    """
181
    if int(duration) in (1, 3, 6, 15, 30):
182
        return int(duration)
183
    else:
184
        raise argparse.ArgumentError(argument=None, message="Invalid duration.")
185
186
187
def droidlookup_hashtype(method):
188
    """
189
    Check if Android autoloader lookup hash type is valid.
190
191
    :param method: None for regular OS links, "sha256/512" for SHA256 or 512 hash.
192
    :type method: str
193
    """
194
    if method.lower() in ("sha512", "sha256"):
195
        return method.lower()
196
    else:
197
        raise argparse.ArgumentError(argument=None, message="Invalid type.")
198
199
200
def droidlookup_devicetype(device):
201
    """
202
    Check if Android autoloader device type is valid.
203
204
    :param device: Android autoloader types to check.
205
    :type device: str
206
    """
207
    devices = ("Priv", "DTEK50")
208
    if device is None:
209
        return None
210
    else:
211
        for dev in devices:
212
            if dev.lower() == device.lower():
213
                return dev
214
        else:
0 ignored issues
show
Bug introduced by
The else clause is not necessary as the loop does not contain a break statement.

If the loop cannot exit early through the use of break, the else part will always be executed. You can therefore just leave off the else.

Loading history...
215
            raise argparse.ArgumentError(argument=None, message="Invalid device.")
216
217
218
def s2b(input_check):
219
    """
220
    Return Boolean interpretation of string input.
221
222
    :param input_check: String to check if it means True or False.
223
    :type input_check: str
224
    """
225
    return str(input_check).lower() in ("yes", "true", "t", "1", "y")
226
227
228
def is_amd64():
229
    """
230
    Check if script is running on an AMD64 system.
231
    """
232
    return platform.machine().endswith("64")
233
234
235
def is_windows():
236
    """
237
    Check if script is running on Windows.
238
    """
239
    return platform.system() == "Windows"
240
241
242
def get_seven_zip(talkative=False):
243
    """
244
    Return name of 7-Zip executable.
245
    On POSIX, it MUST be 7za.
246
    On Windows, it can be installed or supplied with the script.
247
    :func:`win_seven_zip` is used to determine if it's installed.
248
249
    :param talkative: Whether to output to screen. False by default.
250
    :type talkative: bool
251
    """
252
    return win_seven_zip(talkative) if is_windows() else "7za"
253
254
255
def win_seven_zip(talkative=False):
256
    """
257
    For Windows, check where 7-Zip is ("where", pretty much).
258
    Consult registry first for any installed instances of 7-Zip.
259
260
    :param talkative: Whether to output to screen. False by default.
261
    :type talkative: bool
262
    """
263
    if talkative:
264
        print("CHECKING INSTALLED FILES...")
265
    try:
266
        import winreg  # windows registry
267
        hk7z = winreg.OpenKey(winreg.HKEY_CURRENT_USER, "Software\\7-Zip")
268
        path = winreg.QueryValueEx(hk7z, "Path")
269
    except OSError as exc:
270
        if talkative:
271
            print("SOMETHING WENT WRONG")
272
            print(str(exc))
273
            print("TRYING LOCAL FILES...")
274
        return win_seven_zip_local(talkative)
275
    else:
276
        if talkative:
277
            print("7ZIP USING INSTALLED FILES")
278
        return '"{0}"'.format(os.path.join(path[0], "7z.exe"))
279
280
281
def win_seven_zip_local(talkative=False):
282
    """
283
    If 7-Zip isn't in the registry, fall back onto supplied executables.
284
    If *those* aren't there, return "error".
285
286
    :param talkative: Whether to output to screen. False by default.
287
    :type talkative: bool
288
    """
289
    listdir = os.listdir(os.getcwd())
290
    filecount = 0
291
    for i in listdir:
292
        if i in ["7za.exe", "7za64.exe"]:
293
            filecount += 1
294
    if filecount == 2:
295
        if talkative:
296
            print("7ZIP USING LOCAL FILES")
297
        if is_amd64():
298
            return "7za64.exe"
299
        else:
300
            return "7za.exe"
301
    else:
302
        if talkative:
303
            print("NO LOCAL FILES")
304
        return "error"
305
306
307
def get_core_count():
308
    """
309
    Find out how many CPU cores this system has.
310
    """
311
    try:
312
        cores = str(compat.enum_cpus())  # 3.4 and up
313
    except NotImplementedError:
314
        cores = "1"  # 3.2-3.3
315
    else:
316
        if compat.enum_cpus() is None:
317
            cores = "1"
318
    return cores
319
320
321
def prep_seven_zip(talkative=False):
322
    """
323
    Check for presence of 7-Zip.
324
    On POSIX, check for p7zip.
325
    On Windows, check for 7-Zip.
326
327
    :param talkative: Whether to output to screen. False by default.
328
    :type talkative: bool
329
    """
330
    if is_windows():
331
        return get_seven_zip(talkative) != "error"
332
    else:
333
        try:
334
            path = compat.where_which("7za")
335
        except ImportError:
336
            if talkative:
337
                print("PLEASE INSTALL SHUTILWHICH WITH PIP")
338
            return False
339
        else:
340
            if path is None:
341
                if talkative:
342
                    print("NO 7ZIP")
343
                    print("PLEASE INSTALL p7zip")
344
                return False
345
            else:
346
                if talkative:
347
                    print("7ZIP FOUND AT {0}".format(path))
348
                return True
349
350
351
def increment(version, inc=3):
352
    """
353
    Increment version by given number. For repeated lookups.
354
355
    :param version: w.x.y.ZZZZ, becomes w.x.y.(ZZZZ + increment).
356
    :type version: str
357
358
    :param inc: What to increment by. Default is 3.
359
    :type inc: str
360
    """
361
    splitos = version.split(".")
362
    splitos[3] = int(splitos[3])
363
    if splitos[3] > 9996:  # prevent overflow
364
        splitos[3] = 0
365
    splitos[3] += int(inc)
366
    splitos[3] = str(splitos[3])
367
    return ".".join(splitos)
368
369
370
def stripper(name):
371
    """
372
    Strip fluff from bar filename.
373
374
    :param name: Bar filename, must contain '-nto+armle-v7+signed.bar'.
375
    :type name: str
376
    """
377
    return name.replace("-nto+armle-v7+signed.bar", "")
378
379
380
def generate_urls(baseurl, osversion, radioversion, core=False):
381
    """
382
    Generate a list of OS URLs and a list of radio URLs based on input.
383
384
    :param baseurl: The URL, from http to the hashed software release.
385
    :type baseurl: str
386
387
    :param osversion: OS version.
388
    :type osversion: str
389
390
    :param radioversion: Radio version.
391
    :type radioversion: str
392
393
    :param core: Whether or not to return core URLs as well.
394
    :type core: bool
395
    """
396
    suffix = "nto+armle-v7+signed.bar"
397
    osurls = [
398
        "{0}/winchester.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix),
399
        "{0}/qc8960.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix),
400
        "{0}/qc8960.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix),
401
        "{0}/qc8974.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix)
402
    ]
403
    radiourls = [
404
        "{0}/m5730-{1}-{2}".format(baseurl, radioversion, suffix),
405
        "{0}/qc8960-{1}-{2}".format(baseurl, radioversion, suffix),
406
        "{0}/qc8960.omadm-{1}-{2}".format(baseurl, radioversion, suffix),
407
        "{0}/qc8960.wtr-{1}-{2}".format(baseurl, radioversion, suffix),
408
        "{0}/qc8960.wtr5-{1}-{2}".format(baseurl, radioversion, suffix),
409
        "{0}/qc8930.wtr5-{1}-{2}".format(baseurl, radioversion, suffix),
410
        "{0}/qc8974.wtr2-{1}-{2}".format(baseurl, radioversion, suffix)
411
    ]
412
    coreurls = []
413
    splitos = osversion.split(".")
414
    splitos = [int(i) for i in splitos]
415
    if splitos[1] >= 4 or (splitos[1] == 3 and splitos[2] >= 1):  # 10.3.1+
416
        osurls[2] = osurls[2].replace("qc8960.factory_sfi", "qc8960.factory_sfi_hybrid_qc8x30")
417
        osurls[3] = osurls[3].replace("qc8974.factory_sfi", "qc8960.factory_sfi_hybrid_qc8974")
418
    if core:
419
        for url in osurls:
420
            coreurls.append(url.replace(".desktop", ""))
421
    return osurls, radiourls, coreurls
422
423
424
def generate_lazy_urls(baseurl, osversion, radioversion, device):
425
    """
426
    Generate a pair of OS/radio URLs based on input.
427
428
    :param baseurl: The URL, from http to the hashed software release.
429
    :type baseurl: str
430
431
    :param osversion: OS version.
432
    :type osversion: str
433
434
    :param radioversion: Radio version.
435
    :type radioversion: str
436
437
    :param device: Device to use.
438
    :type device: int
439
    """
440
    suffix = "nto+armle-v7+signed.bar"
441
    splitos = [int(i) for i in osversion.split(".")]
442
    if device == 0:
443
        osurl = "{0}/winchester.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix)
444
        radiourl = "{0}/m5730-{1}-{2}".format(baseurl, radioversion, suffix)
445
    elif device == 1:
446
        osurl = "{0}/qc8960.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix)
447
        radiourl = "{0}/qc8960-{1}-{2}".format(baseurl, radioversion, suffix)
448
    elif device == 2:
449
        osurl = "{0}/qc8960.verizon_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix)
450
        radiourl = "{0}/qc8960.omadm-{1}-{2}".format(baseurl, radioversion, suffix)
451
    elif device == 3:
452
        osurl = "{0}/qc8960.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix)
453
        radiourl = "{0}/qc8960.wtr-{1}-{2}".format(baseurl, radioversion, suffix)
454
    elif device == 4:
455
        osurl = "{0}/qc8960.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix)
456
        radiourl = "{0}/qc8960.wtr5-{1}-{2}".format(baseurl, radioversion, suffix)
457
    elif device == 5:
458
        osurl = "{0}/qc8960.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix)
459
        radiourl = "{0}/qc8930.wtr5-{1}-{2}".format(baseurl, radioversion, suffix)
460
        if (splitos[1] >= 4) or (splitos[1] == 3 and splitos[2] >= 1):
461
            osurl = osurl.replace("qc8960.factory_sfi", "qc8960.factory_sfi_hybrid_qc8x30")
462
    elif device == 6:
463
        osurl = "{0}/qc8974.factory_sfi.desktop-{1}-{2}".format(baseurl, osversion, suffix)
464
        radiourl = "{0}/qc8974.wtr2-{1}-{2}".format(baseurl, radioversion, suffix)
465
        if (splitos[1] >= 4) or (splitos[1] == 3 and splitos[2] >= 1):
466
            osurl = osurl.replace("qc8974.factory_sfi", "qc8960.factory_sfi_hybrid_qc8974")
467
    return osurl, radiourl
468
469
470
def bulk_urls(baseurl, osversion, radioversion, core=False, alturl=None):
471
    """
472
    Generate all URLs, plus extra Verizon URLs.
473
474
    :param baseurl: The URL, from http to the hashed software release.
475
    :type baseurl: str
476
477
    :param osversion: OS version.
478
    :type osversion: str
479
480
    :param radioversion: Radio version.
481
    :type radioversion: str
482
483
    :param device: Device to use.
484
    :type device: int
485
486
    :param core: Whether or not to return core URLs as well.
487
    :type core: bool
488
489
    :param alturl: The base URL for any alternate radios.
490
    :type alturl: str
491
    """
492
    osurls, radurls, coreurls = generate_urls(baseurl, osversion, radioversion, core)
493
    vzwos, vzwrad = generate_lazy_urls(baseurl, osversion, radioversion, 2)
494
    osurls.append(vzwos)
495
    radurls.append(vzwrad)
496
    vzwcore = vzwos.replace("sfi.desktop", "sfi")
497
    if core:
498
        coreurls.append(vzwcore)
499
    osurls = list(set(osurls))  # pop duplicates
500
    radurls = list(set(radurls))
501
    if core:
502
        coreurls = list(set(coreurls))
503
    if alturl is not None:
504
        radiourls2 = []
505
        for rad in radurls:
506
            radiourls2.append(rad.replace(baseurl, alturl))
507
        radurls = radiourls2
508
        del radiourls2
509
    return osurls, coreurls, radurls
510
511
512
def line_begin():
513
    """
514
    Go to beginning of line, to overwrite whatever's there.
515
    """
516
    sys.stdout.write("\r")
517
    sys.stdout.flush()
518
519
520
def spinner_clear():
521
    """
522
    Get rid of any spinner residue left in stdout.
523
    """
524
    sys.stdout.write("\b \b")
525
    sys.stdout.flush()
526
527
528
class Spinner(object):
529
    """
530
    A basic spinner using itertools. No need for progress.
531
    """
532
533
    def __init__(self):
534
        self.wheel = itertools.cycle(['-', '/', '|', '\\'])
535
        self.file = dummy.UselessStdout()
536
537
    def after(self):
538
        """
539
        Iterate over itertools.cycle, write to file.
540
        """
541
        try:
542
            self.file.write(next(self.wheel))
543
            self.file.flush()
544
            self.file.write("\b\r")
545
            self.file.flush()
546
        except (KeyboardInterrupt, SystemExit):  # pragma: no cover
547
            self.stop()
548
549
    def stop(self):
550
        """
551
        Kill output.
552
        """
553
        self.file = dummy.UselessStdout()
554
555
556
class SpinManager(object):
557
    """
558
    Wraps around the itertools spinner, runs it in another thread.
559
    """
560
561
    def __init__(self):
562
        spinner = Spinner()
563
        self.spinner = spinner
564
        self.thread = threading.Thread(target=self.loop, args=())
565
        self.thread.daemon = True
566
        self.scanning = False
567
        self.spinner.file = dummy.UselessStdout()
568
569
    def start(self):
570
        """
571
        Begin the spinner.
572
        """
573
        self.spinner.file = sys.stderr
574
        self.scanning = True
575
        self.thread.start()
576
577
    def loop(self):
578
        """
579
        Spin if scanning, clean up if not.
580
        """
581
        while self.scanning:
582
            time.sleep(0.5)
583
            try:
584
                line_begin()
585
                self.spinner.after()
586
            except (KeyboardInterrupt, SystemExit):  # pragma: no cover
587
                self.scanning = False
588
                self.stop()
589
590
    def stop(self):
591
        """
592
        Stop the spinner.
593
        """
594
        self.spinner.stop()
595
        self.scanning = False
596
        spinner_clear()
597
        line_begin()
598
        if not is_windows():  # pragma: no cover
599
            print("\n")
600
601
602
def return_and_delete(target):
603
    """
604
    Read text file, then delete it. Return contents.
605
606
    :param target: Text file to read.
607
    :type target: str
608
    """
609
    with open(target, "r") as thefile:
610
        content = thefile.read()
611
    os.remove(target)
612
    return content
613
614
615
def verify_loader_integrity(loaderfile):
616
    """
617
    Test for created loader integrity. Windows-only.
618
619
    :param loaderfile: Path to loader.
620
    :type loaderfile: str
621
    """
622
    if not is_windows():
623
        pass
624
    else:
625
        excode = None
626
        try:
627
            with open(os.devnull, 'rb') as dnull:
628
                cmd = "{0} fileinfo".format(loaderfile)
629
                excode = subprocess.call(cmd, stdout=dnull, stderr=subprocess.STDOUT)
630
        except OSError:
631
            excode = -1
632
        return excode == 0  # 0 if OK, non-zero if something broke
633
634
635
def verify_bulk_loaders(a_dir):
636
    """
637
    Run :func:`verify_loader_integrity` for all files in a dir.
638
639
    :param a_dir: Directory to use.
640
    :type a_dir: str
641
    """
642
    if not is_windows():
643
        pass
644
    else:
645
        files = [file for file in os.listdir(a_dir) if not os.path.isdir(file)]
646
        brokens = []
647
        for file in files:
648
            if file.endswith(".exe") and file.startswith(bbconstants.PREFIXES):
649
                print("TESTING: {0}".format((file)))
650
                if not verify_loader_integrity(file):
651
                    brokens.append(file)
652
        return brokens
653
654
655
def workers(input_data):
656
    """
657
    Count number of CPU workers, smaller of number of threads and length of data.
658
659
    :param input_data: Input data, some iterable.
660
    :type input_data: list
661
    """
662
    runners = len(input_data) if len(input_data) < compat.enum_cpus() else compat.enum_cpus()
663
    return runners
664
665
666
def prep_logfile():
667
    """
668 View Code Duplication
    Prepare log file, labeling it with current date. Select folder based on frozen status.
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
669
    """
670
    logfile = "{0}.txt".format(time.strftime("%Y_%m_%d_%H%M%S"))
671
    root = os.getcwd() if getattr(sys, 'frozen', False) else os.path.expanduser("~")
672
    basefolder = os.path.join(root, "lookuplogs")
673
    os.makedirs(basefolder, exist_ok=True)
674
    record = os.path.join(basefolder, logfile)
675
    open(record, "w").close()
676
    return record
677
678
679
def prepends(file, pre, suf):
680
    """
681
    Check if filename starts with/ends with stuff.
682
683
    :param file: File to check.
684
    :type file: str
685
686
    :param pre: Prefix(es) to check.
687
    :type pre: str or list or tuple
688
689
    :param suf: Suffix(es) to check.
690
    :type suf: str or list or tuple
691
    """
692
    return file.startswith(pre) and file.endswith(suf)
693
694
695
def lprint(iterable):
696
    """
697
    A oneliner for 'for item in x: print item'.
698
699
    :param iterable: Iterable to print.
700
    :type iterable: list/tuple
701
    """
702
    for item in iterable:
703
        print(item)
704
705
706
def cappath_config_loader(homepath=None):
707
    """
708
    Read a ConfigParser file to get cap preferences.
709
710
    :param homepath: Folder containing ini file. Default is user directory.
711
    :type homepath: str
712
    """
713
    config = configparser.ConfigParser()
714
    homepath = os.path.expanduser("~") if homepath is None else homepath
715
    conffile = os.path.join(homepath, "bbarchivist.ini")
716
    if not os.path.exists(conffile):
717
        open(conffile, 'w').close()
718
    config.read(conffile)
719
    if not config.has_section('cap'):
720
        config['cap'] = {}
721
    capini = config['cap']
722
    cappath = capini.get('path', fallback=bbconstants.CAP.location)
723
    return cappath
724
725
726
def cappath_config_writer(cappath=None, homepath=None):
727
    """
728
    Write a ConfigParser file to store cap preferences.
729
730
    :param cappath: Method to use.
731
    :type cappath: str
732
733
    :param homepath: Folder containing ini file. Default is user directory.
734
    :type homepath: str
735
    """
736
    if cappath is None:
737
        cappath = grab_cap()
738
    config = configparser.ConfigParser()
739
    if homepath is None:
740
        homepath = os.path.expanduser("~")
741
    conffile = os.path.join(homepath, "bbarchivist.ini")
742
    if not os.path.exists(conffile):
743
        open(conffile, 'w').close()
744
    config.read(conffile)
745
    if not config.has_section('cap'):
746
        config['cap'] = {}
747
    config['cap']['path'] = cappath
748
    with open(conffile, "w") as configfile:
749
        config.write(configfile)
750