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 ( cfccd9...b77fc8 )
by Gonzalo
27s
created

_CondaAPI.dependencies()   D

Complexity

Conditions 8

Size

Total Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

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