GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Push — master ( 71fa57...cfccd9 )
by Gonzalo
02:47 queued 01:20
created

_CondaAPI.create_from_yaml()   A

Complexity

Conditions 1

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
c 1
b 0
f 0
dl 0
loc 17
rs 9.4285
1
# -*- coding: utf-8 -*-
2
3
"""
4
Updated `conda-api` to include additional methods, queued worker processes
5
calling `QProcess` instead of `subprocess.Popen`.
6
"""
7
8
# Standard library imports
9
from os.path import basename, isdir, join
10
from collections import deque
11
import json
12
import os
13
import platform
14
import re
15
import sys
16
import time
17
import yaml
18
19
# Third party imports
20
from qtpy.QtCore import QByteArray, QObject, QProcess, QTimer, Signal
21
22
# Local imports
23
from conda_manager.utils.findpip import PIP_LIST_SCRIPT
24
from conda_manager.utils.logs import logger
25
26
27
__version__ = '1.3.0'
28
29
30
# --- Errors
31
# -----------------------------------------------------------------------------
32
class PipError(Exception):
33
    """General pip error."""
34
    pass
35
36
37
class CondaError(Exception):
38
    """General Conda error."""
39
    pass
40
41
42
class CondaProcessWorker(CondaError):
43
    """General Conda error."""
44
    pass
45
46
47
class CondaEnvExistsError(CondaError):
48
    """Conda environment already exists."""
49
    pass
50
51
52
# --- Helpers
53
# -----------------------------------------------------------------------------
54
PY2 = sys.version[0] == '2'
55
PY3 = sys.version[0] == '3'
56
DEBUG = False
57
58
59
def to_text_string(obj, encoding=None):
60
    """Convert `obj` to (unicode) text string."""
61
    if PY2:
62
        # Python 2
63
        if encoding is None:
64
            return unicode(obj)
65
        else:
66
            return unicode(obj, encoding)
67
    else:
68
        # Python 3
69
        if encoding is None:
70
            return str(obj)
71
        elif isinstance(obj, str):
72
            # In case this function is not used properly, this could happen
73
            return obj
74
        else:
75
            return str(obj, encoding)
76
77
78
def handle_qbytearray(obj, encoding):
79
    """
80
    Qt/Python3 compatibility helper.
81
    """
82
    if isinstance(obj, QByteArray):
83
        obj = obj.data()
84
85
    return to_text_string(obj, encoding=encoding)
86
87
88
class ProcessWorker(QObject):
89
    """
90
    """
91
    sig_finished = Signal(object, object, object)
92
    sig_partial = Signal(object, object, object)
93
94
    def __init__(self, cmd_list, parse=False, pip=False, callback=None,
95
                 extra_kwargs={}):
96
        super(ProcessWorker, self).__init__()
97
        self._result = None
98
        self._cmd_list = cmd_list
99
        self._parse = parse
100
        self._pip = pip
101
        self._conda = not pip
102
        self._callback = callback
103
        self._fired = False
104
        self._communicate_first = False
105
        self._partial_stdout = None
106
        self._extra_kwargs = extra_kwargs
107
108
        self._timer = QTimer()
109
        self._process = QProcess()
110
111
        self._timer.setInterval(150)
112
113
        self._timer.timeout.connect(self._communicate)
114
        # self._process.finished.connect(self._communicate)
115
        self._process.readyReadStandardOutput.connect(self._partial)
116
117
    def _partial(self):
118
        raw_stdout = self._process.readAllStandardOutput()
119
        stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8)
120
121
        json_stdout = stdout.replace('\n\x00', '')
122
        try:
123
            json_stdout = json.loads(json_stdout)
124
        except Exception:
125
            json_stdout = stdout
126
127
        if self._partial_stdout is None:
128
            self._partial_stdout = stdout
129
        else:
130
            self._partial_stdout += stdout
131
132
        self.sig_partial.emit(self, json_stdout, None)
133
134
    def _communicate(self):
135
        """
136
        """
137
        if not self._communicate_first:
138
            if self._process.state() == QProcess.NotRunning:
139
                self.communicate()
140
        elif self._fired:
141
            self._timer.stop()
142
143
    def communicate(self):
144
        """
145
        """
146
        self._communicate_first = True
147
        self._process.waitForFinished()
148
149
        if self._partial_stdout is None:
150
            raw_stdout = self._process.readAllStandardOutput()
151
            stdout = handle_qbytearray(raw_stdout, _CondaAPI.UTF8)
152
        else:
153
            stdout = self._partial_stdout
154
155
        raw_stderr = self._process.readAllStandardError()
156
        stderr = handle_qbytearray(raw_stderr, _CondaAPI.UTF8)
157
        result = [stdout.encode(_CondaAPI.UTF8), stderr.encode(_CondaAPI.UTF8)]
158
159
        # FIXME: Why does anaconda client print to stderr???
160
        if PY2:
161
            stderr = stderr.decode()
162
        if 'using anaconda cloud api site' not in stderr.lower():
163
            if stderr.strip() and self._conda:
164
                raise Exception('{0}:\n'
165
                                'STDERR:\n{1}\nEND'
166
                                ''.format(' '.join(self._cmd_list),
167
                                          stderr))
168
#            elif stderr.strip() and self._pip:
169
#                raise PipError(self._cmd_list)
170
        else:
171
            result[-1] = ''
172
173
        if self._parse and stdout:
174
            try:
175
                result = json.loads(stdout), result[-1]
176
            except ValueError as error:
177
                result = stdout, error
178
179
            if 'error' in result[0]:
180
                error = '{0}: {1}'.format(" ".join(self._cmd_list),
181
                                          result[0]['error'])
182
                result = result[0], error
183
184
        if self._callback:
185
            result = self._callback(result[0], result[-1],
186
                                    **self._extra_kwargs), result[-1]
187
188
        self._result = result
189
        self.sig_finished.emit(self, result[0], result[-1])
190
191
        if result[-1]:
192
            logger.error(str(('error', result[-1])))
193
194
        self._fired = True
195
196
        return result
197
198
    def close(self):
199
        """
200
        """
201
        self._process.close()
202
203
    def is_finished(self):
204
        """
205
        """
206
        return self._process.state() == QProcess.NotRunning and self._fired
207
208
    def start(self):
209
        """
210
        """
211
        logger.debug(str(' '.join(self._cmd_list)))
212
213
        if not self._fired:
214
            self._partial_ouput = None
215
            self._process.start(self._cmd_list[0], self._cmd_list[1:])
216
            self._timer.start()
217
        else:
218
            raise CondaProcessWorker('A Conda ProcessWorker can only run once '
219
                                     'per method call.')
220
221
222
# --- API
223
# -----------------------------------------------------------------------------
224
class _CondaAPI(QObject):
225
    """
226
    """
227
    ROOT_PREFIX = None
228
    ENCODING = 'ascii'
229
    UTF8 = 'utf-8'
230
    DEFAULT_CHANNELS = ['https://repo.continuum.io/pkgs/pro',
231
                        'https://repo.continuum.io/pkgs/free']
232
233
    def __init__(self, parent=None):
234
        super(_CondaAPI, self).__init__()
235
        self._parent = parent
236
        self._queue = deque()
237
        self._timer = QTimer()
238
        self._current_worker = None
239
        self._workers = []
240
241
        self._timer.setInterval(1000)
242
        self._timer.timeout.connect(self._clean)
243
244
        self.set_root_prefix()
245
246
    def _clean(self):
247
        """
248
        Periodically check for inactive workers and remove their references.
249
        """
250
        if self._workers:
251
            for w in self._workers:
252
                if w.is_finished():
253
                    self._workers.remove(w)
254
        else:
255
            self._current_worker = None
256
            self._timer.stop()
257
258
    def _start(self):
259
        """
260
        """
261
        if len(self._queue) == 1:
262
            self._current_worker = self._queue.popleft()
263
            self._workers.append(self._current_worker)
264
            self._current_worker.start()
265
            self._timer.start()
266
267
    def is_active(self):
268
        """
269
        Check if a worker is still active.
270
        """
271
        return len(self._workers) == 0
272
273
    def terminate_all_processes(self):
274
        """
275
        Kill all working processes.
276
        """
277
        for worker in self._workers:
278
            worker.close()
279
280
    # --- Conda api
281
    # -------------------------------------------------------------------------
282
    def _call_conda(self, extra_args, abspath=True, parse=False,
283
                    callback=None):
284
        """
285
        Call conda with the list of extra arguments, and return the worker.
286
        The result can be force by calling worker.communicate(), which returns
287
        the tuple (stdout, stderr).
288
        """
289
        if abspath:
290
            if sys.platform == 'win32':
291
                python = join(self.ROOT_PREFIX, 'python.exe')
292
                conda = join(self.ROOT_PREFIX, 'Scripts',
293
                             'conda-script.py')
294
            else:
295
                python = join(self.ROOT_PREFIX, 'bin/python')
296
                conda = join(self.ROOT_PREFIX, 'bin/conda')
297
            cmd_list = [python, conda]
298
        else:
299
            # Just use whatever conda is on the path
300
            cmd_list = ['conda']
301
302
        cmd_list.extend(extra_args)
303
304
        process_worker = ProcessWorker(cmd_list, parse=parse,
305
                                       callback=callback)
306
        process_worker.sig_finished.connect(self._start)
307
        self._queue.append(process_worker)
308
        self._start()
309
310
        return process_worker
311
312
    def _call_and_parse(self, extra_args, abspath=True, callback=None):
313
        """
314
        """
315
        return self._call_conda(extra_args, abspath=abspath, parse=True,
316
                                callback=callback)
317
318
    def _setup_install_commands_from_kwargs(self, kwargs, keys=tuple()):
319
        cmd_list = []
320
        if kwargs.get('override_channels', False) and 'channel' not in kwargs:
321
            raise TypeError('conda search: override_channels requires channel')
322
323
        if 'env' in kwargs:
324
            cmd_list.extend(['--name', kwargs.pop('env')])
325
        if 'prefix' in kwargs:
326
            cmd_list.extend(['--prefix', kwargs.pop('prefix')])
327
        if 'channel' in kwargs:
328
            channel = kwargs.pop('channel')
329
            if isinstance(channel, str):
330
                cmd_list.extend(['--channel', channel])
331
            else:
332
                cmd_list.append('--channel')
333
                cmd_list.extend(channel)
334
335
        for key in keys:
336
            if key in kwargs and kwargs[key]:
337
                cmd_list.append('--' + key.replace('_', '-'))
338
339
        return cmd_list
340
341
    def set_root_prefix(self, prefix=None):
342
        """
343
        Set the prefix to the root environment (default is /opt/anaconda).
344
        This function should only be called once (right after importing
345
        conda_api).
346
        """
347
        if prefix:
348
            self.ROOT_PREFIX = prefix
349
        else:
350
            # Find some conda instance, and then use info to get 'root_prefix'
351
            worker = self._call_and_parse(['info', '--json'], abspath=False)
352
            info = worker.communicate()[0]
353
            self.ROOT_PREFIX = info['root_prefix']
354
355
    def get_conda_version(self):
356
        """
357
        Return the version of conda being used (invoked) as a string.
358
        """
359
        return self._call_conda(['--version'],
360
                                callback=self._get_conda_version)
361
362
    def _get_conda_version(self, stdout, stderr):
363
        # argparse outputs version to stderr in Python < 3.4.
364
        # http://bugs.python.org/issue18920
365
        pat = re.compile(r'conda:?\s+(\d+\.\d\S+|unknown)')
366
        m = pat.match(stderr.decode().strip())
367
        if m is None:
368
            m = pat.match(stdout.decode().strip())
369
370
        if m is None:
371
            raise Exception('output did not match: {0}'.format(stderr))
372
373
        return m.group(1)
374
375
    def get_envs(self):
376
        """
377
        Return all of the (named) environment (this does not include the root
378
        environment), as a list of absolute path to their prefixes.
379
        """
380
        logger.debug('')
381
#        return self._call_and_parse(['info', '--json'],
382
#                                    callback=lambda o, e: o['envs'])
383
        envs = os.listdir(os.sep.join([self.ROOT_PREFIX, 'envs']))
384
        envs = [os.sep.join([self.ROOT_PREFIX, 'envs', i]) for i in envs]
385
386
        valid_envs = [e for e in envs if os.path.isdir(e) and
387
                      self.environment_exists(prefix=e)]
388
389
        return valid_envs
390
391
    def get_prefix_envname(self, name):
392
        """
393
        Given the name of an environment return its full prefix path, or None
394
        if it cannot be found.
395
        """
396
        prefix = None
397
        if name == 'root':
398
            prefix = self.ROOT_PREFIX
399
400
#        envs, error = self.get_envs().communicate()
401
        envs = self.get_envs()
402
        for p in envs:
403
            if basename(p) == name:
404
                prefix = p
405
406
        return prefix
407
408
    def linked(self, prefix):
409
        """
410
        Return the (set of canonical names) of linked packages in `prefix`.
411
        """
412
        logger.debug(str(prefix))
413
414
        if not isdir(prefix):
415
            raise Exception('no such directory: {0}'.format(prefix))
416
417
        meta_dir = join(prefix, 'conda-meta')
418
        if not isdir(meta_dir):
419
            # We might have nothing in linked (and no conda-meta directory)
420
            return set()
421
422
        return set(fn[:-5] for fn in os.listdir(meta_dir)
423
                   if fn.endswith('.json'))
424
425
    def split_canonical_name(self, cname):
426
        """
427
        Split a canonical package name into (name, version, build) strings.
428
        """
429
        return tuple(cname.rsplit('-', 2))
430
431
    def info(self, abspath=True):
432
        """
433
        Return a dictionary with configuration information.
434
        No guarantee is made about which keys exist.  Therefore this function
435
        should only be used for testing and debugging.
436
        """
437
        logger.debug(str(''))
438
        return self._call_and_parse(['info', '--json'], abspath=abspath)
439
440
    def package_info(self, package, abspath=True):
441
        """
442
        Return a dictionary with package information.
443
        """
444
        return self._call_and_parse(['info', package, '--json'],
445
                                    abspath=abspath)
446
447 View Code Duplication
    def search(self, regex=None, spec=None, **kwargs):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
448
        """
449
        Search for packages.
450
        """
451
        cmd_list = ['search', '--json']
452
453
        if regex and spec:
454
            raise TypeError('conda search: only one of regex or spec allowed')
455
456
        if regex:
457
            cmd_list.append(regex)
458
459
        if spec:
460
            cmd_list.extend(['--spec', spec])
461
462
        if 'platform' in kwargs:
463
            cmd_list.extend(['--platform', kwargs.pop('platform')])
464
465
        cmd_list.extend(
466
            self._setup_install_commands_from_kwargs(
467
                kwargs,
468
                ('canonical', 'unknown', 'use_index_cache', 'outdated',
469
                 'override_channels')))
470
471
        return self._call_and_parse(cmd_list,
472
                                    abspath=kwargs.get('abspath', True))
473
474
475
    def create_from_yaml(self, name, yamlfile):
476
        """
477
        Create new environment using conda-env to allow creation to a yaml spec.
478
479
        Unlike other methods, this calls conda-env, and requires a named
480
        environment and uses channels as defined in rcfiles.
481
482
        Parameters
483
        ----------
484
        name : string
485
            Environment name
486
        yamlfile : string
487
            Path to yaml file with package spec (as created by conda env export
488
        """
489
        logger.debug(str((name, yamlfile)))
490
        cmd_list = ['env', 'create', '-n', name, '-f', yamlfile]
491
        return self._call_and_parse(cmd_list)
492
493
    def create(self, name=None, prefix=None, pkgs=None, channels=None):
494
        """
495
        Create an environment either by name or path with a specified set of
496
        packages.
497
        """
498
        logger.debug(str((prefix, pkgs, channels)))
499
500
        # TODO: Fix temporal hack
501
        if not pkgs or not isinstance(pkgs, (list, tuple, str)):
502
            raise TypeError('must specify a list of one or more packages to '
503
                            'install into new environment')
504
505
        cmd_list = ['create', '--yes', '--quiet', '--json', '--mkdir']
506
        if name:
507
            ref = name
508
            search = [os.path.join(d, name) for d in
509
                      self.info().communicate()[0]['envs_dirs']]
510
            cmd_list.extend(['--name', name])
511
        elif prefix:
512
            ref = prefix
513
            search = [prefix]
514
            cmd_list.extend(['--prefix', prefix])
515
        else:
516
            raise TypeError('must specify either an environment name or a '
517
                            'path for new environment')
518
519
        if any(os.path.exists(prefix) for prefix in search):
520
            raise CondaEnvExistsError('Conda environment {0} already '
521
                                      'exists'.format(ref))
522
523
        # TODO: Fix temporal hack
524
        if isinstance(pkgs, (list, tuple)):
525
            cmd_list.extend(pkgs)
526
        elif isinstance(pkgs, str):
527
            cmd_list.extend(['--file', pkgs])
528
529
        # TODO: Check if correct
530
        if channels:
531
            cmd_list.extend(['--override-channels'])
532
533
            for channel in channels:
534
                cmd_list.extend(['--channel'])
535
                cmd_list.extend([channel])
536
537
        return self._call_and_parse(cmd_list)
538
539
    def parse_token_channel(self, channel, token):
540
        """
541
        Adapt a channel to include the authentication token of the logged
542
        user.
543
544
        Ignore default channels
545
        """
546
        if token and channel not in self.DEFAULT_CHANNELS:
547
            url_parts = channel.split('/')
548
            start = url_parts[:-1]
549
            middle = 't/{0}'.format(token)
550
            end = url_parts[-1]
551
            token_channel = '{0}/{1}/{2}'.format('/'.join(start), middle, end)
552
            return token_channel
553
        else:
554
            return channel
555
556
    def install(self, name=None, prefix=None, pkgs=None, dep=True,
557
                channels=None, token=None):
558
        """
559
        Install packages into an environment either by name or path with a
560
        specified set of packages.
561
562
        If token is specified, the channels different from the defaults will
563
        get the token appended.
564
        """
565
        logger.debug(str((prefix, pkgs, channels)))
566
567
        # TODO: Fix temporal hack
568
        if not pkgs or not isinstance(pkgs, (list, tuple, str)):
569
            raise TypeError('must specify a list of one or more packages to '
570
                            'install into existing environment')
571
572
        cmd_list = ['install', '--yes', '--json', '--force-pscheck']
573
        if name:
574
            cmd_list.extend(['--name', name])
575
        elif prefix:
576
            cmd_list.extend(['--prefix', prefix])
577
        else:
578
            # Just install into the current environment, whatever that is
579
            pass
580
581
        # TODO: Check if correct
582
        if channels:
583
            cmd_list.extend(['--override-channels'])
584
585
            for channel in channels:
586
                cmd_list.extend(['--channel'])
587
                channel = self.parse_token_channel(channel, token)
588
                cmd_list.extend([channel])
589
590
        # TODO: Fix temporal hack
591
        if isinstance(pkgs, (list, tuple)):
592
            cmd_list.extend(pkgs)
593
        elif isinstance(pkgs, str):
594
            cmd_list.extend(['--file', pkgs])
595
596
        if not dep:
597
            cmd_list.extend(['--no-deps'])
598
599
        return self._call_and_parse(cmd_list)
600
601
    def update(self, *pkgs, **kwargs):
602
        """
603
        Update package(s) (in an environment) by name.
604
        """
605
        cmd_list = ['update', '--json', '--quiet', '--yes']
606
607
        if not pkgs and not kwargs.get('all'):
608
            raise TypeError("Must specify at least one package to update, or "
609
                            "all=True.")
610
611
        cmd_list.extend(
612
            self._setup_install_commands_from_kwargs(
613
                kwargs,
614
                ('dry_run', 'no_deps', 'override_channels',
615
                 'no_pin', 'force', 'all', 'use_index_cache', 'use_local',
616
                 'alt_hint')))
617
618
        cmd_list.extend(pkgs)
619
620
        return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath',
621
                                                                 True))
622
623
    def remove(self, name=None, prefix=None, pkgs=None, all_=False):
624
        """
625
        Remove a package (from an environment) by name.
626
627
        Returns {
628
            success: bool, (this is always true),
629
            (other information)
630
        }
631
        """
632
        logger.debug(str((prefix, pkgs)))
633
634
        cmd_list = ['remove', '--json', '--quiet', '--yes']
635
636
        if not pkgs and not all_:
637
            raise TypeError("Must specify at least one package to remove, or "
638
                            "all=True.")
639
640
        if name:
641
            cmd_list.extend(['--name', name])
642
        elif prefix:
643
            cmd_list.extend(['--prefix', prefix])
644 View Code Duplication
        else:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
645
            raise TypeError('must specify either an environment name or a '
646
                            'path for package removal')
647
648
        if all_:
649
            cmd_list.extend(['--all'])
650
        else:
651
            cmd_list.extend(pkgs)
652
653
        return self._call_and_parse(cmd_list)
654
655
    def remove_environment(self, name=None, path=None, **kwargs):
656
        """
657
        Remove an environment entirely.
658
659
        See ``remove``.
660
        """
661
        return self.remove(name=name, path=path, all=True, **kwargs)
662
663
    def clone_environment(self, clone, name=None, prefix=None, **kwargs):
664
        """
665
        Clone the environment `clone` into `name` or `prefix`.
666
        """
667
        cmd_list = ['create', '--json', '--quiet']
668
669
        if (name and prefix) or not (name or prefix):
670
            raise TypeError("conda clone_environment: exactly one of `name` "
671
                            "or `path` required")
672
673
        if name:
674
            cmd_list.extend(['--name', name])
675
676
        if prefix:
677
            cmd_list.extend(['--prefix', prefix])
678
679
        cmd_list.extend(['--clone', clone])
680
681
        cmd_list.extend(
682
            self._setup_install_commands_from_kwargs(
683
                kwargs,
684
                ('dry_run', 'unknown', 'use_index_cache', 'use_local',
685
                 'no_pin', 'force', 'all', 'channel', 'override_channels',
686
                 'no_default_packages')))
687
688
        return self._call_and_parse(cmd_list, abspath=kwargs.get('abspath',
689
                                                                 True))
690
691
    # FIXME:
692
    def process(self, name=None, prefix=None, cmd=None):
693
        """
694
        Create a Popen process for cmd using the specified args but in the
695
        conda environment specified by name or prefix.
696
697
        The returned object will need to be invoked with p.communicate() or
698
        similar.
699
        """
700
        if bool(name) == bool(prefix):
701
            raise TypeError('exactly one of name or prefix must be specified')
702
703
        if not cmd:
704
            raise TypeError('cmd to execute must be specified')
705
706
        if not args:
707
            args = []
708
709
        if name:
710
            prefix = self.get_prefix_envname(name)
711
712
        conda_env = dict(os.environ)
713
        sep = os.pathsep
714
715
        if sys.platform == 'win32':
716
            conda_env['PATH'] = join(prefix,
717
                                     'Scripts') + sep + conda_env['PATH']
718
        else:
719
            # Unix
720
            conda_env['PATH'] = join(prefix, 'bin') + sep + conda_env['PATH']
721
722
        conda_env['PATH'] = prefix + os.pathsep + conda_env['PATH']
723
724
        cmd_list = [cmd]
725
        cmd_list.extend(args)
726
727
#         = self.subprocess.process(cmd_list, env=conda_env, stdin=stdin,
728
#                                        stdout=stdout, stderr=stderr)
729
730
    def _setup_config_from_kwargs(self, kwargs):
731
        cmd_list = ['--json', '--force']
732
733
        if 'file' in kwargs:
734
            cmd_list.extend(['--file', kwargs['file']])
735
736
        if 'system' in kwargs:
737
            cmd_list.append('--system')
738
739
        return cmd_list
740
741
    def config_path(self, **kwargs):
742
        """
743
        Get the path to the config file.
744
        """
745
        cmd_list = ['config', '--get']
746
        cmd_list.extend(self._setup_config_from_kwargs(kwargs))
747
748
        return self._call_and_parse(cmd_list,
749
                                    abspath=kwargs.get('abspath', True),
750
                                    callback=lambda o, e: o['rc_path'])
751
752
    def config_get(self, *keys, **kwargs):
753
        """
754
        Get the values of configuration keys.
755
756
        Returns a dictionary of values. Note, the key may not be in the
757
        dictionary if the key wasn't set in the configuration file.
758
        """
759
        cmd_list = ['config', '--get']
760
        cmd_list.extend(keys)
761
        cmd_list.extend(self._setup_config_from_kwargs(kwargs))
762
763
        return self._call_and_parse(cmd_list,
764
                                    abspath=kwargs.get('abspath', True),
765
                                    callback=lambda o, e: o['get'])
766
767
    def config_set(self, key, value, **kwargs):
768
        """
769
        Set a key to a (bool) value.
770
771
        Returns a list of warnings Conda may have emitted.
772
        """
773
        cmd_list = ['config', '--set', key, str(value)]
774
        cmd_list.extend(self._setup_config_from_kwargs(kwargs))
775
776
        return self._call_and_parse(
777
            cmd_list,
778
            abspath=kwargs.get('abspath', True),
779
            callback=lambda o, e: o.get('warnings', []))
780
781
    def config_add(self, key, value, **kwargs):
782
        """
783
        Add a value to a key.
784
785
        Returns a list of warnings Conda may have emitted.
786
        """
787
        cmd_list = ['config', '--add', key, value]
788
        cmd_list.extend(self._setup_config_from_kwargs(kwargs))
789
790
        return self._call_and_parse(
791
            cmd_list,
792
            abspath=kwargs.get('abspath', True),
793
            callback=lambda o, e: o.get('warnings', []))
794
795
    def config_remove(self, key, value, **kwargs):
796
        """
797
        Remove a value from a key.
798
799
        Returns a list of warnings Conda may have emitted.
800
        """
801
        cmd_list = ['config', '--remove', key, value]
802
        cmd_list.extend(self._setup_config_from_kwargs(kwargs))
803
804
        return self._call_and_parse(
805
            cmd_list,
806
            abspath=kwargs.get('abspath', True),
807
            callback=lambda o, e: o.get('warnings', []))
808
809
    def config_delete(self, key, **kwargs):
810
        """
811
        Remove a key entirely.
812
813
        Returns a list of warnings Conda may have emitted.
814
        """
815
        cmd_list = ['config', '--remove-key', key]
816
        cmd_list.extend(self._setup_config_from_kwargs(kwargs))
817
818
        return self._call_and_parse(
819
            cmd_list,
820
            abspath=kwargs.get('abspath', True),
821
            callback=lambda o, e: o.get('warnings', []))
822
823
    def run(self, command, abspath=True):
824
        """
825
        Launch the specified app by name or full package name.
826
827
        Returns a dictionary containing the key "fn", whose value is the full
828
        package (ending in ``.tar.bz2``) of the app.
829
        """
830
        cmd_list = ['run', '--json', command]
831
832
        return self._call_and_parse(cmd_list, abspath=abspath)
833
834
    # --- Additional methods
835
    # -----------------------------------------------------------------------------
836
    def dependencies(self, name=None, prefix=None, pkgs=None, channels=None,
837
                     dep=True):
838
        """
839
        Get dependenciy list for packages to be installed into an environment
840
        defined either by 'name' or 'prefix'.
841
        """
842
        if not pkgs or not isinstance(pkgs, (list, tuple)):
843
            raise TypeError('must specify a list of one or more packages to '
844
                            'install into existing environment')
845
846
        cmd_list = ['install', '--dry-run', '--json', '--force-pscheck']
847
848
        if not dep:
849
            cmd_list.extend(['--no-deps'])
850
851
        if name:
852
            cmd_list.extend(['--name', name])
853
        elif prefix:
854
            cmd_list.extend(['--prefix', prefix])
855
        else:
856
            pass
857
858
        cmd_list.extend(pkgs)
859
860
        # TODO: Check if correct
861
        if channels:
862
            cmd_list.extend(['--override-channels'])
863
864
            for channel in channels:
865
                cmd_list.extend(['--channel'])
866
                cmd_list.extend([channel])
867
868
        return self._call_and_parse(cmd_list)
869
870
    def environment_exists(self, name=None, prefix=None, abspath=True):
871
        """
872
        Check if an environment exists by 'name' or by 'prefix'. If query is
873
        by 'name' only the default conda environments directory is searched.
874
        """
875
        logger.debug(str((name, prefix)))
876
877
        if name and prefix:
878
            raise TypeError("Exactly one of 'name' or 'prefix' is required.")
879
880
        if name:
881
            prefix = self.get_prefix_envname(name)
882
883
        if prefix is None:
884
            prefix = self.ROOT_PREFIX
885
886
        return os.path.isdir(os.path.join(prefix, 'conda-meta'))
887
888
    def clear_lock(self, abspath=True):
889
        """
890
        Clean any conda lock in the system.
891
        """
892
        cmd_list = ['clean', '--lock', '--json']
893
        return self._call_and_parse(cmd_list, abspath=abspath)
894
895
    def package_version(self, prefix=None, name=None, pkg=None):
896
        """
897
        """
898
        package_versions = {}
899
900
        if name and prefix:
901
            raise TypeError("Exactly one of 'name' or 'prefix' is required.")
902
903
        if name:
904
            prefix = self.get_prefix_envname(name)
905
906
        if self.environment_exists(prefix=prefix):
907
908
            for package in self.linked(prefix):
909
                if pkg in package:
910
                    n, v, b = self.split_canonical_name(package)
911
                    package_versions[n] = v
912
913
        return package_versions.get(pkg, None)
914
915
    def get_platform(self):
916
        """
917
        Get platform of current system (system and bitness).
918
        """
919
        _sys_map = {'linux2': 'linux', 'linux': 'linux',
920
                    'darwin': 'osx', 'win32': 'win', 'openbsd5': 'openbsd'}
921
922
        non_x86_linux_machines = {'armv6l', 'armv7l', 'ppc64le'}
923
        sys_platform = _sys_map.get(sys.platform, 'unknown')
924
        bits = 8 * tuple.__itemsize__
925
926
        if (sys_platform == 'linux' and
927
                platform.machine() in non_x86_linux_machines):
928
            arch_name = platform.machine()
929
            subdir = 'linux-{0}'.format(arch_name)
930
        else:
931
            arch_name = {64: 'x86_64', 32: 'x86'}[bits]
932
            subdir = '{0}-{1}'.format(sys_platform, bits)
933
934
        return subdir
935
936
    def get_condarc_channels(self):
937
        """
938
        Returns all the channel urls defined in .condarc using the defined
939
        `channel_alias`.
940
941
        If no condarc file is found, use the default channels.
942
        """
943
        # First get the location of condarc file and parse it to get
944
        # the channel alias and the channels.
945
        default_channel_alias = 'https://conda.anaconda.org'
946
        default_urls = ['https://repo.continuum.io/pkgs/free',
947
                        'https://repo.continuum.io/pkgs/pro']
948
949
        condarc_path = os.path.abspath(os.path.expanduser('~/.condarc'))
950
        channels = default_urls[:]
951
952
        if not os.path.isfile(condarc_path):
953
            condarc = None
954
            channel_alias = default_channel_alias
955
        else:
956
            with open(condarc_path, 'r') as f:
957
                data = f.read()
958
                condarc = yaml.load(data)
959
                channels += condarc.get('channels', [])
960
                channel_alias = condarc.get('channel_alias',
961
                                            default_channel_alias)
962
963
        if channel_alias[-1] == '/':
964
            template = "{0}{1}"
965
        else:
966
            template = "{0}/{1}"
967
968
        if 'defaults' in channels:
969
            channels.remove('defaults')
970
971
        channel_urls = []
972
        for channel in channels:
973
            if not channel.startswith('http'):
974
                channel_url = template.format(channel_alias, channel)
975
            else:
976
                channel_url = channel
977
            channel_urls.append(channel_url)
978
979
        return channel_urls
980
981
    # --- Pip commands
982
    # -------------------------------------------------------------------------
983
    def _call_pip(self, name=None, prefix=None, extra_args=None,
984
                  callback=None):
985
        """ """
986
        cmd_list = self._pip_cmd(name=name, prefix=prefix)
987
        cmd_list.extend(extra_args)
988
989
        process_worker = ProcessWorker(cmd_list, pip=True, callback=callback)
990
        process_worker.sig_finished.connect(self._start)
991
        self._queue.append(process_worker)
992
        self._start()
993
994
        return process_worker
995
996
    def _pip_cmd(self, name=None, prefix=None):
997
        """
998
        Get pip location based on environment `name` or `prefix`.
999
        """
1000
        if (name and prefix) or not (name or prefix):
1001
            raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' "
1002
                            "required.")
1003
1004
        if name and self.environment_exists(name=name):
1005
            prefix = self.get_prefix_envname(name)
1006
1007
        if sys.platform == 'win32':
1008
            python = join(prefix, 'python.exe')  # FIXME:
1009
            pip = join(prefix, 'pip.exe')        # FIXME:
1010
        else:
1011
            python = join(prefix, 'bin/python')
1012
            pip = join(prefix, 'bin/pip')
1013
1014
        cmd_list = [python, pip]
1015
1016
        return cmd_list
1017
1018
    def pip_list(self, name=None, prefix=None, abspath=True):
1019
        """
1020
        Get list of pip installed packages.
1021
        """
1022
        if (name and prefix) or not (name or prefix):
1023
            raise TypeError("conda pip: exactly one of 'name' ""or 'prefix' "
1024
                            "required.")
1025
1026
        if name:
1027
            prefix = self.get_prefix_envname(name)
1028
1029
        pip_command = os.sep.join([prefix, 'bin', 'python'])
1030
        cmd_list = [pip_command, PIP_LIST_SCRIPT]
1031
        process_worker = ProcessWorker(cmd_list, pip=True, parse=True,
1032
                                       callback=self._pip_list,
1033
                                       extra_kwargs={'prefix': prefix})
1034
        process_worker.sig_finished.connect(self._start)
1035
        self._queue.append(process_worker)
1036
        self._start()
1037
1038
        return process_worker
1039
1040
#        if name:
1041
#            cmd_list = ['list', '--name', name]
1042
#        if prefix:
1043
#            cmd_list = ['list', '--prefix', prefix]
1044
1045
#        return self._call_conda(cmd_list, abspath=abspath,
1046
#                                callback=self._pip_list)
1047
1048
    def _pip_list(self, stdout, stderr, prefix=None):
1049
        """
1050
        """
1051
        result = stdout  # A dict
1052
        linked = self.linked(prefix)
1053
1054
        pip_only = []
1055
1056
        linked_names = [self.split_canonical_name(l)[0] for l in linked]
1057
1058
        for pkg in result:
1059
            name = self.split_canonical_name(pkg)[0]
1060
            if name not in linked_names:
1061
                pip_only.append(pkg)
1062
            # FIXME: NEED A MORE ROBUST WAY!
1063
#            if '<pip>' in line and '#' not in line:
1064
#                temp = line.split()[:-1] + ['pip']
1065
#                temp = '-'.join(temp)
1066
#                if '-(' in temp:
1067
#                    start = temp.find('-(')
1068
#                    end = temp.find(')')
1069
#                    substring = temp[start:end+1]
1070
#                    temp = temp.replace(substring, '')
1071
#                result.append(temp)
1072
1073
        return pip_only
1074
1075
    def pip_remove(self, name=None, prefix=None, pkgs=None):
1076
        """
1077
        Remove a pip package in given environment by `name` or `prefix`.
1078
        """
1079
        logger.debug(str((prefix, pkgs)))
1080
1081
        if isinstance(pkgs, list) or isinstance(pkgs, tuple):
1082
            pkg = ' '.join(pkgs)
1083
        else:
1084
            pkg = pkgs
1085
1086
        extra_args = ['uninstall', '--yes', pkg]
1087
1088
        return self._call_pip(name=name, prefix=prefix, extra_args=extra_args)
1089
1090
    def pip_search(self, search_string=None):
1091
        """
1092
        Search for pip installable python packages in PyPI matching
1093
        `search_string`.
1094
        """
1095
        extra_args = ['search', search_string]
1096
        return self._call_pip(name='root', extra_args=extra_args,
1097
                              callback=self._pip_search)
1098
1099
        # if stderr:
1100
        #     raise PipError(stderr)
1101
        # You are using pip version 7.1.2, however version 8.0.2 is available.
1102
        # You should consider upgrading via the 'pip install --upgrade pip'
1103
        # command.
1104
1105
    def _pip_search(self, stdout, stderr):
1106
        result = {}
1107
        lines = to_text_string(stdout).split('\n')
1108
        while '' in lines:
1109
            lines.remove('')
1110
1111
        for line in lines:
1112
            if ' - ' in line:
1113
                parts = line.split(' - ')
1114
                name = parts[0].strip()
1115
                description = parts[1].strip()
1116
                result[name] = description
1117
1118
        return result
1119
1120
1121
CONDA_API = None
1122
1123
1124
def CondaAPI():
1125
    global CONDA_API
1126
1127
    if CONDA_API is None:
1128
        CONDA_API = _CondaAPI()
1129
1130
    return CONDA_API
1131
1132
COUNTER = 0
1133
1134
1135
def ready_print(worker, output, error):
1136
    global COUNTER
1137
    COUNTER += 1
1138
    print(COUNTER, output, error)
1139
1140
1141
def test():
1142
    """
1143
    """
1144
    from conda_manager.utils.qthelpers import qapplication
1145
1146
    app = qapplication()
1147
    conda_api = CondaAPI()
1148
#    print(conda_api.get_condarc_channels())
1149
#    worker = conda_api.info()
1150
##    worker.sig_finished.connect(ready_print)
1151
#    worker = conda_api.info()
1152
#    worker = conda_api.info()
1153
#    worker = conda_api.info()
1154
#    worker = conda_api.info()
1155
#    worker = conda_api.info()
1156
#    worker = conda_api.info()
1157
#    worker = conda_api.info()
1158
#    worker = conda_api.info()
1159
#    worker = conda_api.info()
1160
#    worker = conda_api.info()
1161
#    worker = conda_api.pip_search('spyder')
1162
#    worker.sig_finished.connect(ready_print)
1163
    worker = conda_api.pip_list(name='py3')
1164
    worker.sig_finished.connect(ready_print)
1165
#    print(conda_api.package_version(name='root', pkg='spyder'))
1166
1167
    app.exec_()
1168
1169
1170
if __name__ == '__main__':
1171
    test()
1172