File: setup_base_environments.py

package info (click to toggle)
suds 1.2.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,836 kB
  • sloc: python: 18,367; makefile: 198; sh: 41
file content (978 lines) | stat: -rw-r--r-- 41,664 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
# -*- coding: utf-8 -*-

# This program is free software; you can redistribute it and/or modify it under
# the terms of the (LGPL) GNU Lesser General Public License as published by the
# Free Software Foundation; either version 3 of the License, or (at your
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Library Lesser General Public License
# for more details at ( http://www.gnu.org/licenses/lgpl.html ).
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
# written by: Jurko Gospodnetić ( jurko.gospodnetic@pke.hr )

"""
Sets up base Python development environments used by this project.

These are the Python environments from which multiple virtual environments can
then be spawned as needed.

The environments should have the following Python packages installed:
  * setuptools (for installing pip)
  * pip (for installing everything except itself)
  * pytest (for running the project's test suite)
  * virtualenv (for creating virtual Python environments)
plus certain specific Python versions may require additional backward
compatibility support packages.

"""
#TODO: Python 3.4.0 comes with setuptools & pip preinstalled but they can be
# installed and/or upgraded manually if needed so we choose not to use their
# built-in installations, i.e. the built-in ensurepip module. Consider using
# the built-in ensurepip module if regular setuptools/pip installation fails
# for some reason or has been configured to run locally.
#TODO: logging
#TODO: command-line option support
#TODO: support for additional configuration files, e.g. ones that are developer
# or development environment specific.
#TODO: warn if multiple environments use the same executable
#TODO: make the script importable
#TODO: report when the installed package version is newer than the last tested
# one
#TODO: hold a list of last tested package versions and report a warning if a
# newer one is encountered
#  > # Where no Python package version has been explicitly specified, the
#  > # following 'currently latest available' package release has been
#  > # successfully used:
#  > "last tested package version": {
#  >     "argparse": "1.2.1",
#  >     "backports.ssl_match_hostname": "3.4.0.2",
#  >     "colorama": "0.3.1",
#  >     "pip": "1.5.5",
#  >     "py": "1.4.20",
#  >     "pytest": "2.5.2",
#  >     "setuptools": "3.6",
#  >     "virtualenv": "1.11.5"},
#TODO: automated checking for new used package versions, e.g. using PyPI XRC
# API:
#  > import xmlrpc.client as xrc
#  > client = xrc.ServerProxy("http://pypi.python.org/pypi")
#  > client.package_releases("six")  # just the latest release
#  > client.package_releases("six", True)  # all releases
#TODO: option to break on bad environments
#TODO: verbose option to report bad environment detection output
#TODO: verbose option to report all environment detection output
#TODO: verbose option to report environment details
#TODO: Consider running the environment scanner script from an empty temporary
# folder to avoid importing random same-named modules from the current working
# folder. An alternative would be to play around with sys.path in the scanner
# script, e.g. remove its first element. Also, we might want to clear out any
# globally set Python environment variables such as PYTHONPATH.
#TODO: collect stdout, stderr & log outputs for each easy_install/pip run
#TODO: configurable - avoid downloads if a suitable locally downloaded source
# is available (ez_setup, setuptools, pip)
#TODO: 244 downloads must come before 243 installation
#TODO: support configuring what latest suitable version we want to use if
# available in the following layers:
#  - already installed
#  - local installation cache (can not find out the latest available content,
#    can only download to it or install from it)
#  - pypi
# related configuration options:
#  - allow already installed,
#  - allow locally downloaded,
#  - allow pypi
#TODO: if you want better local-cache support - use devpi:
#  - better caching support and version detection
#  - devpi & pypi usage transparent
#  - we'll need a script for installing & setting up the devpi server
#TODO: parallelization
#TODO: concurrency support - file locking required for:
# installation cache folder:
#   downloading (write)
#   zipping eggs (write)
#   installing from local folder (read)
# Python environment installation area:
#   installing new packages (write)
#   running Python code (read)
#TODO: test whether we can upgrade pip in-place
#TODO: test how we can make pip safe to use when there are multiple pip based
# installations being run at the same time by the same user - specify some
# global folders, like the temp build folder, explicitly
#TODO: Detect most recent packages on PyPI, but do that at most once in a
# single script run, or with a separate script, or use devpi. Currently, if a
# suitable package is found locally, a more suitable one will not be checked
# for on PyPI.
#TODO: Recheck error handling to make sure all failed commands are correctly
# detected. Some might not set a non-0 exit code on error and so their output
# must be used as a success/failure indicator.

import itertools
import os
import os.path
import re
import sys
import tempfile

from suds_devel.configuration import BadConfiguration, Config, configparser
from suds_devel.egg import zip_eggs_in_folder
from suds_devel.environment import BadEnvironment, Environment
from suds_devel.exception import EnvironmentSetupError
from suds_devel.parse_version import parse_version
from suds_devel.requirements import pytest_requirements, virtualenv_requirements
import suds_devel.utility as utility


# -------------
# Configuration
# -------------

class MyConfig(Config):

    # Section names.
    SECTION__ACTIONS = "setup base environments - actions"
    SECTION__FOLDERS = "setup base environments - folders"
    SECTION__REUSE_PREINSTALLED_SETUPTOOLS =  \
        "setup base environments - reuse pre-installed setuptools"

    def __init__(self, script, project_folder, ini_file):
        """
        Initialize new script configuration.

        External configuration parameters may be specified relative to the
        following folders:
          * script - relative to the current working folder
          * project_folder - relative to the script folder
          * ini_file - relative to the project folder

        """
        super(MyConfig, self).__init__(script, project_folder, ini_file)
        self.__cached_paths = {}
        try:
            self.__read_configuration()
        except configparser.Error:
            raise BadConfiguration(sys.exc_info()[1].message)

    def ez_setup_folder(self):
        return self.__get_cached_path("ez_setup folder")

    def installation_cache_folder(self):
        return self.__get_cached_path("installation cache")

    def pip_download_cache_folder(self):
        return self.__get_cached_path("pip download cache")

    def __get_cached_path(self, option):
        try:
            return self.__cached_paths[option]
        except KeyError:
            x = self.__cached_paths[option] = self.__get_path(option)
            return x

    def __get_path(self, option):
        try:
            folder = self._reader.get(self.SECTION__FOLDERS, option)
        except (configparser.NoOptionError, configparser.NoSectionError):
            return
        except configparser.Error:
            raise BadConfiguration("Error reading configuration option "
                "'%s.%s' - %s" % (self.SECTION__FOLDERS, option,
                sys.exc_info()[1]))
        base_paths = {
            "project-folder": self.project_folder,
            "script-folder": self.script_folder,
            "ini-folder": os.path.dirname(self.ini_file)}
        folder_parts = re.split("[\\/]{2}", folder, maxsplit=1)
        base_path = None
        if len(folder_parts) == 2:
            base_path = base_paths.get(folder_parts[0].lower())
            if base_path is not None:
                folder = folder_parts[1]
        if not folder:
            raise BadConfiguration("Configuration option '%s.%s' invalid. A "
                "valid relative path must not be empty. Use '.' to represent "
                "the base folder." % (section, option))
        if base_path is None:
            base_path = base_paths.get("ini-folder")
        return os.path.normpath(os.path.join(base_path, folder))

    def __read_configuration(self):
        self._read_environment_configuration()

        section = self.SECTION__REUSE_PREINSTALLED_SETUPTOOLS
        self.reuse_old_setuptools = self._get_bool(section, "old")
        self.reuse_best_setuptools = self._get_bool(section, "best")
        self.reuse_future_setuptools = self._get_bool(section, "future")

        section = self.SECTION__ACTIONS
        self.report_environment_configuration = (
            self._get_bool(section, "report environment configuration"))
        self.report_raw_environment_scan_results = (
            self._get_bool(section, "report raw environment scan results"))
        self.setup_setuptools = (
            self._get_tribool(section, "setup setuptools"))
        self.download_installations = (
            self._get_tribool(section, "download installations"))
        self.install_environments = (
            self._get_tribool(section, "install environments"))


def _prepare_configuration():
    # We know we are a regular stand-alone script file and not an imported
    # module (either frozen, imported from disk, zip-file, external database or
    # any other source). That means we can safely assume we have the __file__
    # attribute available.
    global config
    config = MyConfig(__file__, "..", "setup.cfg")


# --------------------
# Environment scanning
# --------------------

def report_environment_configuration(env):
    if not (env and env.initial_scan_completed):
        return
    print("  ctypes version: %s" % (env.ctypes_version,))
    print("  pip version: %s" % (env.pip_version,))
    print("  pytest version: %s" % (env.pytest_version,))
    print("  python version: %s" % (env.python_version,))
    print("  setuptools version: %s" % (env.setuptools_version,))
    if env.setuptools_zipped_egg is not None:
        print("  setuptools zipped egg: %s" % (env.setuptools_zipped_egg,))
    print("  virtualenv version: %s" % (env.virtualenv_version,))


def report_raw_environment_scan_results(out, err, exit_code):
    if out is None and err is None and exit_code is None:
        return
    print("-----------------------------------")
    print("--- RAW SCAN RESULTS --------------")
    print("-----------------------------------")
    if exit_code is not None:
        print("*** EXIT CODE: %d" % (exit_code,))
    for name, value in (("STDOUT", out), ("STDERR", err)):
        if value:
            print("*** %s:" % (name,))
            sys.stdout.write(value)
            if value[-1] != "\n":
                sys.stdout.write("\n")
    print("-----------------------------------")


class ScanProgressReporter:
    """
    Reports scanning progress to the user.

    Takes care of all the gory progress output formatting details so they do
    not pollute the actual scanning logic implementation.

    A ScanProgressReporter's output formatting logic assumes that the reporter
    is the one with full output control between calls to a its report_start() &
    report_finish() methods. Therefore, user code must not do any custom output
    during that time or it risks messing up the reporter's output formatting.

    """

    def __init__(self, environments):
        self.__max_name_length = max(len(x.name()) for x in environments)
        self.__count = len(environments)
        self.__count_width = len(str(self.__count))
        self.__current = 0
        self.__reporting = False
        print("Scanning Python environments...")

    def report_start(self, name):
        assert len(name) <= self.__max_name_length
        assert self.__current <= self.__count
        assert not self.__reporting
        self.__reporting = True
        self.__current += 1
        name_padding = " " * (self.__max_name_length - len(name))
        sys.stdout.write("[%*d/%d] Scanning '%s'%s - " % (self.__count_width,
            self.__current, self.__count, name, name_padding))
        sys.stdout.flush()

    def report_finish(self, report):
        assert self.__reporting
        self.__reporting = False
        print(report)


class ScannedEnvironmentTracker:
    """Helps track scanned Python environments and report duplicates."""

    def __init__(self):
        self.__names = set()
        self.__last_name = None
        self.__environments = []

    def environments(self):
        return self.__environments

    def track_environment(self, env):
        assert env not in self.__environments
        assert env.name() == self.__last_name
        self.__environments.append(env)

    def track_name(self, name):
        if name in self.__names:
            raise BadConfiguration("Python environment '%s' configured "
                "multiple times." % (name,))
        self.__names.add(name)
        self.__last_name = name


def scan_python_environment(env, progress_reporter, environment_tracker):
    environment_tracker.track_name(env.name())
    # N.B. No custom output allowed between calls to our progress_reporter's
    # report_start() & report_finish() methods or we risk messing up its output
    # formatting.
    progress_reporter.report_start(env.name())
    try:
        try:
            out, err, exit_code = env.run_initial_scan()
        except:
            progress_reporter.report_finish("----- %s" % (_exc_str(),))
            raise
    except BadEnvironment:
        out, err, exit_code = sys.exc_info()[1].raw_scan_results()
    else:
        progress_reporter.report_finish(env.description())
        environment_tracker.track_environment(env)
    if config.report_raw_environment_scan_results:
        report_raw_environment_scan_results(out, err, exit_code)
    if config.report_environment_configuration:
        report_environment_configuration(env)


def scan_python_environments():
    environments = config.python_environments
    if not environments:
        raise BadConfiguration("No Python environments configured.")
    progress_reporter = ScanProgressReporter(environments)
    environment_tracker = ScannedEnvironmentTracker()
    for env in environments:
        scan_python_environment(env, progress_reporter, environment_tracker)
    return environment_tracker.environments()


# ------------------------------------------
# Generic functionality local to this module
# ------------------------------------------

def _create_installation_cache_folder_if_needed():
    assert config.installation_cache_folder() is not None
    if not os.path.isdir(config.installation_cache_folder()):
        print("Creating installation cache folder...")
        # os.path.abspath() to avoid ".." entries in the path that would
        # otherwise confuse os.makedirs().
        os.makedirs(os.path.abspath(config.installation_cache_folder()))


def _exc_str():
    exc_type, exc = sys.exc_info()[:2]
    type_desc = []
    if exc_type.__module__ and exc_type.__module__ != "__main__":
        type_desc.append(exc_type.__module__)
    type_desc.append(exc_type.__name__)
    desc = ".".join(type_desc), str(exc)
    return ": ".join(x for x in desc if x)


def _report_configuration():
    folder = config.installation_cache_folder()
    if folder is not None:
        print("Installation cache folder: '%s'" % (folder,))
    folder = config.pip_download_cache_folder()
    if folder is not None:
        print("PIP download cache folder: '%s'" % (folder,))


def _report_startup_information():
    print("Running in folder: '%s'" % (os.getcwd(),))


# ----------------------------------
# Processing setuptools installation
# ----------------------------------

def process_setuptools(env, actions):
    if "setup setuptools" not in actions:
        return
    installer = _ez_setup_script(env)
    if _reuse_pre_installed_setuptools(env, installer):
        return
    _avoid_setuptools_zipped_egg_upgrade_issue(env, installer)
    try:
        # 'ez_setup' script will download its setuptools installation to the
        # 'current working folder'. If we are using an installation cache
        # folder, we run the script from there to get the downloaded setuptools
        # installation stored together with all of the other used
        # installations. If we are not, then just have it downloaded to the
        # current folder.
        if config.installation_cache_folder() is not None:
            _create_installation_cache_folder_if_needed()
        installer.execute(cwd=config.installation_cache_folder())
    except (KeyboardInterrupt, SystemExit):
        raise
    except Exception:
        raise EnvironmentSetupError("setuptools installation failed - %s" % (
            _exc_str(),))


class _ez_setup_script:
    """setuptools project's ez_setup installer script."""

    def __init__(self, env):
        self.__env = env
        if not config.ez_setup_folder():
            self.__error("ez_setup folder not configured")
        self.__ez_setup_folder = config.ez_setup_folder()
        self.__cached_script_path = None
        self.__cached_setuptools_version = None
        if not os.path.isfile(self.script_path()):
            self.__error("installation script '%s' not found" % (
                self.script_path(),))

    def execute(self, cwd=None):
        script_path = self.script_path()
        kwargs = {}
        if cwd:
            kwargs["cwd"] = cwd
            script_path = os.path.abspath(script_path)
        self.__env.execute([script_path], **kwargs)

    def script_path(self):
        if self.__cached_script_path is None:
            self.__cached_script_path = self.__script_path()
        return self.__cached_script_path

    def setuptools_version(self):
        if self.__cached_setuptools_version is None:
            self.__cached_setuptools_version = self.__setuptools_version()
        return self.__cached_setuptools_version

    def __error(self, msg):
        raise EnvironmentSetupError("Can not install setuptools - %s." % (
            msg,))

    def __script_path(self):
        import suds_devel.ez_setup_versioned as ez
        script_name = ez.script_name(self.__env.sys_version_info)
        return os.path.join(self.__ez_setup_folder, script_name)

    def __setuptools_version(self):
        """Read setuptools version from the underlying ez_setup script."""
        # Read the script directly as a file instead of importing it as a
        # Python module and reading the value from the loaded module's global
        # DEFAULT_VERSION variable. Not all ez_setup scripts are compatible
        # with all Python environments and so importing them would require
        # doing so using a separate process run in the target Python
        # environment instead of the current one.
        f = open(self.script_path(), "r")
        try:
            matcher = re.compile(r'\s*DEFAULT_VERSION\s*=\s*"([^"]*)"\s*$')
            for i, line in enumerate(f):
                if i > 50:
                    break
                match = matcher.match(line)
                if match:
                    return match.group(1)
        finally:
            f.close()
        self.__error("error parsing setuptools installation script '%s'" % (
            self.script_path(),))


def _avoid_setuptools_zipped_egg_upgrade_issue(env, ez_setup):
    """
    Avoid the setuptools self-upgrade issue.

    setuptools versions prior to version 3.5.2 have a bug that can cause their
    upgrade installations to fail when installing a new zipped egg distribution
    over an existing zipped egg setuptools distribution with the same name.

    The following Python versions are not affected by this issue:
      Python 2.4 - use setuptools 1.4.2 - installs itself as a non-zipped egg
      Python 2.6+ - use setuptools versions not affected by this issue
    That just leaves Python versions 2.5.x to worry about.

    This problem occurs because of an internal stale cache issue causing the
    upgrade to read data from the new zip archive at a location calculated
    based on the original zip archive's content, effectively causing such read
    operations to either succeed (if read content had not changed its
    location), fail with a 'bad local header' exception or even fail silently
    and return incorrect data.

    To avoid the issue, we explicitly uninstall the previously installed
    setuptools distribution before installing its new version.

    """
    if env.sys_version_info[:2] != (2, 5):
        return  # only Python 2.5.x affected by this
    if not env.setuptools_zipped_egg:
        return  # setuptools not pre-installed as a zipped egg
    pv_new = parse_version(ez_setup.setuptools_version())
    if pv_new != parse_version(env.setuptools_version):
        return  # issue avoided since zipped egg archive names will not match
    fixed_version = utility.lowest_version_string_with_prefix("3.5.2")
    if pv_new >= parse_version(fixed_version):
        return  # issue fixed in setuptools
    # We could check for pip and use it for a cleaner setuptools uninstall if
    # available, but YAGNI since only Python 2.5.x environments are affected by
    # the zipped egg upgrade issue.
    os.remove(env.setuptools_zipped_egg)


def _reuse_pre_installed_setuptools(env, installer):
    """
    Return whether a pre-installed setuptools distribution should be reused.

    """
    if not env.setuptools_version:
        return  # no prior setuptools ==> no reuse
    reuse_old = config.reuse_old_setuptools
    reuse_best = config.reuse_best_setuptools
    reuse_future = config.reuse_future_setuptools
    reuse_comment = None
    if reuse_old or reuse_best or reuse_future:
        pv_old = parse_version(env.setuptools_version)
        pv_new = parse_version(installer.setuptools_version())
        if pv_old < pv_new:
            if reuse_old:
                reuse_comment = "%s+ recommended" % (
                    installer.setuptools_version(),)
        elif pv_old > pv_new:
            if reuse_future:
                reuse_comment = "%s+ required" % (
                    installer.setuptools_version(),)
        elif reuse_best:
            reuse_comment = ""
    if reuse_comment is None:
        return  # reuse not allowed by configuration
    if reuse_comment:
        reuse_comment = " (%s)" % (reuse_comment,)
    print("Reusing pre-installed setuptools %s distribution%s." % (
        env.setuptools_version, reuse_comment))
    return True  # reusing pre-installed setuptools


# ---------------------------
# Processing pip installation
# ---------------------------

def calculate_pip_requirements(env_version_info):
    # pip releases supported on older Python versions:
    #   * Python 2.4.x - pip 1.1.
    #   * Python 2.5.x - pip 1.3.1.
    pip_version = None
    if env_version_info < (2, 5):
        pip_version = "1.1"
    elif env_version_info < (2, 6):
        pip_version = "1.3.1"
    requirement_spec = utility.requirement_spec
    requirements = [requirement_spec("pip", pip_version)]
    # Although pip claims to be compatible with Python 3.0 & 3.1 it does not
    # seem to work correctly from within such clean Python environments.
    #   * Tested using pip 1.5.4 & Python 3.1.3.
    #   * pip can be installed using Python 3.1.3 ('py313 -m easy_install pip')
    #     but attempting to use it or even just import its pip Python module
    #     fails.
    #   * The problem is caused by a bug in pip's backward compatibility
    #     support implementation, but can be worked around by installing the
    #     backports.ssl_match_hostname package from PyPI.
    if (3,) <= env_version_info < (3, 2):
        requirements.append(requirement_spec("backports.ssl_match_hostname"))
    return requirements


def download_pip(env, requirements):
    """Download pip and its requirements using setuptools."""
    if config.installation_cache_folder() is None:
        raise EnvironmentSetupError("Local installation cache folder not "
            "defined but required for downloading a pip installation.")
    # Installation cache folder needs to be explicitly created for setuptools
    # to be able to copy its downloaded installation files into it. Seen using
    # Python 2.4.4 & setuptools 1.4.
    _create_installation_cache_folder_if_needed()
    try:
        env.execute(["-m", "easy_install", "--zip-ok", "--multi-version",
            "--always-copy", "--exclude-scripts", "--install-dir",
            config.installation_cache_folder()] + requirements)
        zip_eggs_in_folder(config.installation_cache_folder())
    except (KeyboardInterrupt, SystemExit):
        raise
    except Exception:
        raise EnvironmentSetupError("pip download failed.")


def setuptools_install_options(local_storage_folder):
    """
    Return options to make setuptools use installations from the given folder.

    No other installation source is allowed.

    """
    if local_storage_folder is None:
        return []
    # setuptools expects its find-links parameter to contain a list of link
    # sources (either local paths, file: URLs pointing to folders or URLs
    # pointing to a file containing HTML links) separated by spaces. That means
    # that, when specifying such items, whether local paths or URLs, they must
    # not contain spaces. The problem can be worked around by using a local
    # file URL, since URLs can contain space characters encoded as '%20' (for
    # more detailed information see below).
    #
    # Any URL referencing a folder needs to be specified with a trailing '/'
    # character in order for setuptools to correctly recognize it as a folder.
    #
    # All this has been tested using Python 2.4.3/2.4.4 & setuptools 1.4/1.4.2
    # as well as Python 3.4 & setuptools 3.3.
    #
    # Supporting paths with spaces - method 1:
    # ----------------------------------------
    # One way would be to prepare a link file and pass an URL referring to that
    # link file. The link file needs to contain a list of HTML link tags
    # (<a href="..."/>), one for every item stored inside the local storage
    # folder. If a link file references a folder whose name matches the desired
    # requirement name, it will be searched recursively (as described in method
    # 2 below).
    #
    # Note that in order for setuptools to recognize a local link file URL
    # correctly, the file needs to be named with the '.html' extension. That
    # will cause the underlying urllib2.open() operation to return the link
    # file's content type as 'text/html' which is required for setuptools to
    # recognize a valid link file.
    #
    # Supporting paths with spaces - method 2:
    # ----------------------------------------
    # Another possible way is to use an URL referring to the local storage
    # folder directly. This will cause setuptools to prepare and use a link
    # file internally - with its content read from a 'index.html' file located
    # in the given local storage folder, if it exists, or constructed so it
    # contains HTML links to all top-level local storage folder items, as
    # described for method 1 above.
    if " " in local_storage_folder:
        find_links_param = utility.path_to_URL(local_storage_folder)
        if find_links_param[-1] != "/":
            find_links_param += "/"
    else:
        find_links_param = local_storage_folder
    return ["-f", find_links_param, "--allow-hosts=None"]


def install_pip(env, requirements):
    """Install pip and its requirements using setuptools."""
    try:
        installation_source_folder = config.installation_cache_folder()
        options = setuptools_install_options(installation_source_folder)
        if installation_source_folder is not None:
            zip_eggs_in_folder(installation_source_folder)
        env.execute(["-m", "easy_install"] + options + requirements)
    except (KeyboardInterrupt, SystemExit):
        raise
    except Exception:
        raise EnvironmentSetupError("pip installation failed.")


def process_pip(env, actions):
    download = "download pip installation" in actions
    install = "run pip installation" in actions
    if not download and not install:
        return
    requirements = calculate_pip_requirements(env.sys_version_info)
    if download:
        download_pip(env, requirements)
    if install:
        install_pip(env, requirements)


# ----------------------------------
# Processing pip based installations
# ----------------------------------

def pip_invocation_arguments(env_version_info):
    """
    Returns Python arguments for invoking pip with a specific Python version.

    Running pip based installations on Python prior to 2.7.
      * pip based installations may be run using:
          python -c "import pip;pip.main()" install <package-name-to-install>
        in addition to the regular command:
          python -m pip install <package-name-to-install>
      * The '-m' option can not be used with certain Python versions prior to
        Python 2.7.
          * Whether this is so also depends on the specific pip version used.
          * Seems to not work with Python 2.4 and pip 1.1.
          * Seems to work fine with Python 2.5.4 and pip 1.3.1.
          * Seems to not work with Python 2.6.6 and pip 1.5.4.

    """
    if (env_version_info < (2, 5)) or ((2, 6) <= env_version_info < (2, 7)):
        return ["-c", "import pip;pip.main()"]
    return ["-m", "pip"]


def pip_requirements_file(requirements):
    janitor = None
    try:
        os_handle, file_path = tempfile.mkstemp(suffix=".pip-requirements",
            text=True)
        requirements_file = os.fdopen(os_handle, "w")
        try:
            janitor = utility.FileJanitor(file_path)
            for line in requirements:
                requirements_file.write(line)
                requirements_file.write("\n")
        finally:
            requirements_file.close()
        return file_path, janitor
    except:
        if janitor:
            janitor.clean()
        raise


def prepare_pip_requirements_file_if_needed(requirements):
    """
    Make requirements be passed to pip via a requirements file if needed.

    We must be careful about how we pass shell operator characters (e.g. '<',
    '>', '|' or '^') included in our command-line arguments or they might cause
    problems if run through an intermediate shell interpreter. If our pip
    requirement specifications contain such characters, we pass them using a
    separate requirements file.

    This problem has been encountered on Windows 7 SP1 x64 using Python 2.4.3,
    2.4.4 & 2.5.4.

    """
    if utility.any_contains_any(requirements, "<>|()&^"):
        file_path, janitor = pip_requirements_file(requirements)
        requirements[:] = ["-r", file_path]
        return janitor


def prepare_pip_requirements(env):
    requirements = list(itertools.chain(
        pytest_requirements(env.sys_version_info, env.ctypes_version),
        virtualenv_requirements(env.sys_version_info)))
    janitor = prepare_pip_requirements_file_if_needed(requirements)
    return requirements, janitor


def pip_download_cache_options(download_cache_folder):
    if download_cache_folder is None:
        return []
    return ["--download-cache=" + download_cache_folder]


def download_pip_based_installations(env, pip_invocation, requirements,
        download_cache_folder):
    """Download requirements for pip based installation."""
    if config.installation_cache_folder() is None:
        raise EnvironmentSetupError("Local installation cache folder not "
            "defined but required for downloading pip based installations.")
    # Installation cache folder needs to be explicitly created for pip to be
    # able to copy its downloaded installation files into it. The same does not
    # hold for pip's download cache folder which gets created by pip on-demand.
    # Seen using Python 3.4.0 & pip 1.5.4.
    _create_installation_cache_folder_if_needed()
    try:
        pip_options = ["install", "-d", config.installation_cache_folder(),
            "--exists-action=i"]
        pip_options.extend(pip_download_cache_options(download_cache_folder))
        # Running pip based installations on Python 2.5.
        #   * Python 2.5 does not come with SSL support enabled by default and
        #     so pip can not use SSL certified downloads from PyPI.
        #   * To work around this either install the
        #     https://pypi.python.org/pypi/ssl package or run pip using the
        #     '--insecure' command-line options.
        #       * Installing the ssl package seems ridden with problems on
        #         Python 2.5 so this workaround has not been tested.
        if (2, 5) <= env.sys_version_info < (2, 6):
            # There are some potential cases where we do not need to use
            # "--insecure", e.g. if the target Python environment already has
            # the 'ssl' module installed. However, detecting whether this is so
            # does not seem to be worth the effort. The only way to detect
            # whether secure download is supported would be to scan the target
            # environment for this information, e.g. setuptools has this
            # information in its pip.backwardcompat.ssl variable - if it is
            # None, the necessary SSL support is not available. But then we
            # would have to be careful:
            #  - not to run the scan if we already know this information from
            #    some previous scan
            #  - to track all actions that could have invalidated our previous
            #    scan results, etc.
            # It just does not seem to be worth the hassle so for now - YAGNI.
            pip_options.append("--insecure")
        env.execute(pip_invocation + pip_options + requirements)
    except (KeyboardInterrupt, SystemExit):
        raise
    except Exception:
        raise EnvironmentSetupError("pip based download failed.")


def run_pip_based_installations(env, pip_invocation, requirements,
        download_cache_folder):
    # 'pip' download caching system usage notes:
    # 1. When not installing from our own local installation storage folder, we
    #    can still use pip's internal download caching system.
    # 2. We must not enable pip's internal download caching system when
    #    installing from our own local installation storage folder. In that
    #    case, pip attempts to populate its cache from our local installation
    #    folder, but that logic fails when our folder contains a wheel (.whl)
    #    distribution. More precisely, it fails attempting to store the wheel
    #    distribution file's content type information. Tested using Python
    #    3.4.0 & pip 1.5.4.
    try:
        pip_options = ["install"]
        if config.installation_cache_folder() is None:
            pip_options.extend(pip_download_cache_options(
                download_cache_folder))
        else:
            # pip allows us to identify a local folder containing predownloaded
            # installation packages using its '-f' command-line option taking
            # an URL parameter. However, it does not require the URL to be
            # URL-quoted and it does not even seem to recognize URLs containing
            # %xx escaped characters. Tested using an installation cache folder
            # path containing spaces with Python 3.4.0 & pip 1.5.4.
            installation_cache_folder_URL = utility.path_to_URL(
                config.installation_cache_folder(), escape=False)
            pip_options.extend(["-f", installation_cache_folder_URL,
                "--no-index"])
        env.execute(pip_invocation + pip_options + requirements)
    except (KeyboardInterrupt, SystemExit):
        raise
    except Exception:
        raise EnvironmentSetupError("pip based installation failed.")


def post_pip_based_installation_fixups(env):
    """Apply simple post-installation fixes for pip installed packages."""
    if env.sys_version_info[:2] == (3, 1):
        from suds_devel.patch_pytest_on_python_31 import patch
        patch(env)


def process_pip_based_installations(env, actions, download_cache_folder):
    download = "download pip based installations" in actions
    install = "run pip based installations" in actions
    if not download and not install:
        return
    pip_invocation = pip_invocation_arguments(env.sys_version_info)
    janitor = None
    try:
        requirements, janitor = prepare_pip_requirements(env)
        if download:
            download_pip_based_installations(env, pip_invocation, requirements,
                download_cache_folder)
        if install:
            run_pip_based_installations(env, pip_invocation, requirements,
                download_cache_folder)
            post_pip_based_installation_fixups(env)
    finally:
        if janitor:
            janitor.clean()


# ------------------------------
# Processing Python environments
# ------------------------------

def enabled_actions_for_env(env):
    """Returns actions to perform when processing the given environment."""
    def enabled(config_value, required):
        if config_value is Config.TriBool.No:
            return False
        if config_value is Config.TriBool.Yes:
            return True
        assert config_value is Config.TriBool.IfNeeded
        return bool(required)

    # Some old Python versions do not support HTTPS downloads and therefore can
    # not download installation packages from PyPI. To run setuptools or pip
    # based installations on such Python versions, all the required
    # installation packages need to be downloaded locally first using a
    # compatible Python version (e.g. Python 2.4.4 for Python 2.4.3) and then
    # installed locally.
    download_supported = not ((2, 4, 3) <= env.sys_version_info < (2, 4, 4))

    local_install = config.installation_cache_folder() is not None

    actions = set()

    pip_required = False
    run_pip_based_installations = enabled(config.install_environments, True)
    if run_pip_based_installations:
        actions.add("run pip based installations")
        pip_required = True
    if download_supported and enabled(config.download_installations,
            local_install and run_pip_based_installations):
        actions.add("download pip based installations")
        pip_required = True

    setuptools_required = False
    run_pip_installation = enabled(config.install_environments, pip_required)
    if run_pip_installation:
        actions.add("run pip installation")
        setuptools_required = True
    if download_supported and enabled(config.download_installations,
            local_install and run_pip_installation):
        actions.add("download pip installation")
        setuptools_required = True

    if enabled(config.setup_setuptools, setuptools_required):
        actions.add("setup setuptools")

    return actions


def print_environment_processing_title(env):
    title_length = 73
    print("-" * title_length)
    title = "--- %s - Python %s " % (env.name(), env.python_version)
    title += "-" * max(0, title_length - len(title))
    print(title)
    print("-" * title_length)


def process_Python_environment(env):
    actions = enabled_actions_for_env(env)
    if not actions:
        return
    print_environment_processing_title(env)
    process_setuptools(env, actions)
    process_pip(env, actions)
    process_pip_based_installations(env, actions,
        config.pip_download_cache_folder())


def process_python_environments(python_environments):
    for env in python_environments:
        try:
            process_Python_environment(env)
        except EnvironmentSetupError:
            utility.report_error(sys.exc_info()[1])


def main():
    try:
        _report_startup_information()
        _prepare_configuration()
        _report_configuration()
        python_environments = scan_python_environments()
    except BadConfiguration:
        utility.report_error(sys.exc_info()[1])
        return -2
    process_python_environments(python_environments)
    return 0


if __name__ == "__main__":
    sys.exit(main())