1
- # -*- coding: utf-8 -*-
2
-
3
1
# pylint: disable=too-many-lines
4
2
5
3
"""Build configuration for rtd."""
4
+
5
+ import copy
6
6
import os
7
7
from contextlib import contextmanager
8
8
9
+ from readthedocs .config .utils import list_to_dict , to_dict
9
10
from readthedocs .projects .constants import DOCUMENTATION_CHOICES
10
11
11
12
from .find import find_one
12
- from .models import Build , Conda , Mkdocs , Python , Sphinx , Submodules
13
+ from .models import (
14
+ Build ,
15
+ Conda ,
16
+ Mkdocs ,
17
+ Python ,
18
+ PythonInstall ,
19
+ PythonInstallRequirements ,
20
+ Sphinx ,
21
+ Submodules ,
22
+ )
13
23
from .parser import ParseError , parse
14
24
from .validation import (
15
25
VALUE_NOT_FOUND ,
16
26
ValidationError ,
17
27
validate_bool ,
18
28
validate_choice ,
19
29
validate_dict ,
30
+ validate_directory ,
20
31
validate_file ,
21
32
validate_list ,
22
33
validate_string ,
31
42
'ConfigError' ,
32
43
'ConfigOptionNotSupportedError' ,
33
44
'InvalidConfig' ,
45
+ 'PIP' ,
46
+ 'SETUPTOOLS' ,
34
47
)
35
48
36
49
ALL = 'all'
50
+ PIP = 'pip'
51
+ SETUPTOOLS = 'setuptools'
37
52
CONFIG_FILENAME_REGEX = r'^\.?readthedocs.ya?ml$'
38
53
39
54
CONFIG_NOT_SUPPORTED = 'config-not-supported'
@@ -145,7 +160,7 @@ class BuildConfigBase:
145
160
146
161
def __init__ (self , env_config , raw_config , source_file ):
147
162
self .env_config = env_config
148
- self .raw_config = raw_config
163
+ self .raw_config = copy . deepcopy ( raw_config )
149
164
self .source_file = source_file
150
165
if os .path .isdir (self .source_file ):
151
166
self .base_path = self .source_file
@@ -245,10 +260,7 @@ def as_dict(self):
245
260
config = {}
246
261
for name in self .PUBLIC_ATTRIBUTES :
247
262
attr = getattr (self , name )
248
- if hasattr (attr , '_asdict' ):
249
- config [name ] = attr ._asdict ()
250
- else :
251
- config [name ] = attr
263
+ config [name ] = to_dict (attr )
252
264
return config
253
265
254
266
def __getattr__ (self , name ):
@@ -465,7 +477,9 @@ def validate_requirements_file(self):
465
477
if not requirements_file :
466
478
return None
467
479
with self .catch_validation_error ('requirements_file' ):
468
- validate_file (requirements_file , self .base_path )
480
+ requirements_file = validate_file (
481
+ requirements_file , self .base_path
482
+ )
469
483
return requirements_file
470
484
471
485
def validate_formats (self ):
@@ -491,9 +505,39 @@ def formats(self):
491
505
@property
492
506
def python (self ):
493
507
"""Python related configuration."""
508
+ python = self ._config ['python' ]
494
509
requirements = self ._config ['requirements_file' ]
495
- self ._config ['python' ]['requirements' ] = requirements
496
- return Python (** self ._config ['python' ])
510
+ python_install = []
511
+
512
+ # Always append a `PythonInstallRequirements` option.
513
+ # If requirements is None, rtd will try to find a requirements file.
514
+ python_install .append (
515
+ PythonInstallRequirements (
516
+ requirements = requirements ,
517
+ )
518
+ )
519
+ if python ['install_with_pip' ]:
520
+ python_install .append (
521
+ PythonInstall (
522
+ path = self .base_path ,
523
+ method = PIP ,
524
+ extra_requirements = python ['extra_requirements' ],
525
+ )
526
+ )
527
+ elif python ['install_with_setup' ]:
528
+ python_install .append (
529
+ PythonInstall (
530
+ path = self .base_path ,
531
+ method = SETUPTOOLS ,
532
+ extra_requirements = [],
533
+ )
534
+ )
535
+
536
+ return Python (
537
+ version = python ['version' ],
538
+ install = python_install ,
539
+ use_system_site_packages = python ['use_system_site_packages' ],
540
+ )
497
541
498
542
@property
499
543
def conda (self ):
@@ -545,7 +589,7 @@ class BuildConfigV2(BuildConfigBase):
545
589
valid_formats = ['htmlzip' , 'pdf' , 'epub' ]
546
590
valid_build_images = ['1.0' , '2.0' , '3.0' , 'stable' , 'latest' ]
547
591
default_build_image = 'latest'
548
- valid_install_options = ['pip' , 'setup.py' ]
592
+ valid_install_method = [PIP , SETUPTOOLS ]
549
593
valid_sphinx_builders = {
550
594
'html' : 'sphinx' ,
551
595
'htmldir' : 'sphinx_htmldir' ,
@@ -668,39 +712,22 @@ def validate_python(self):
668
712
self .get_valid_python_versions (),
669
713
)
670
714
671
- with self .catch_validation_error ('python.requirements' ):
672
- requirements = self .defaults .get ('requirements_file' )
673
- requirements = self .pop_config ('python.requirements' , requirements )
674
- if requirements != '' and requirements is not None :
675
- requirements = validate_file (requirements , self .base_path )
676
- python ['requirements' ] = requirements
677
-
678
715
with self .catch_validation_error ('python.install' ):
679
- install = (
680
- 'setup.py' if self .defaults .get ('install_project' ) else None
681
- )
682
- install = self .pop_config ('python.install' , install )
683
- if install is not None :
684
- validate_choice (install , self .valid_install_options )
685
- python ['install_with_setup' ] = install == 'setup.py'
686
- python ['install_with_pip' ] = install == 'pip'
687
-
688
- with self .catch_validation_error ('python.extra_requirements' ):
689
- extra_requirements = self .pop_config (
690
- 'python.extra_requirements' ,
691
- [],
692
- )
693
- extra_requirements = validate_list (extra_requirements )
694
- if extra_requirements and not python ['install_with_pip' ]:
695
- self .error (
696
- 'python.extra_requirements' ,
697
- 'You need to install your project with pip '
698
- 'to use extra_requirements' ,
699
- code = PYTHON_INVALID ,
716
+ raw_install = self .raw_config .get ('python' , {}).get ('install' , [])
717
+ validate_list (raw_install )
718
+ if raw_install :
719
+ # Transform to a dict, so it's easy to validate extra keys.
720
+ self .raw_config .setdefault ('python' , {})['install' ] = (
721
+ list_to_dict (raw_install )
700
722
)
701
- python ['extra_requirements' ] = [
702
- validate_string (extra ) for extra in extra_requirements
703
- ]
723
+ else :
724
+ self .pop_config ('python.install' )
725
+
726
+ raw_install = self .raw_config .get ('python' , {}).get ('install' , [])
727
+ python ['install' ] = [
728
+ self .validate_python_install (index )
729
+ for index in range (len (raw_install ))
730
+ ]
704
731
705
732
with self .catch_validation_error ('python.system_packages' ):
706
733
system_packages = self .defaults .get (
@@ -715,6 +742,60 @@ def validate_python(self):
715
742
716
743
return python
717
744
745
+ def validate_python_install (self , index ):
746
+ """Validates the python.install.{index} key."""
747
+ python_install = {}
748
+ key = 'python.install.{}' .format (index )
749
+ raw_install = self .raw_config ['python' ]['install' ][str (index )]
750
+ with self .catch_validation_error (key ):
751
+ validate_dict (raw_install )
752
+
753
+ if 'requirements' in raw_install :
754
+ requirements_key = key + '.requirements'
755
+ with self .catch_validation_error (requirements_key ):
756
+ requirements = validate_file (
757
+ self .pop_config (requirements_key ),
758
+ self .base_path
759
+ )
760
+ python_install ['requirements' ] = requirements
761
+ elif 'path' in raw_install :
762
+ path_key = key + '.path'
763
+ with self .catch_validation_error (path_key ):
764
+ path = validate_directory (
765
+ self .pop_config (path_key ),
766
+ self .base_path
767
+ )
768
+ python_install ['path' ] = path
769
+
770
+ method_key = key + '.method'
771
+ with self .catch_validation_error (method_key ):
772
+ method = validate_choice (
773
+ self .pop_config (method_key , PIP ),
774
+ self .valid_install_method
775
+ )
776
+ python_install ['method' ] = method
777
+
778
+ extra_req_key = key + '.extra_requirements'
779
+ with self .catch_validation_error (extra_req_key ):
780
+ extra_requirements = validate_list (
781
+ self .pop_config (extra_req_key , [])
782
+ )
783
+ if extra_requirements and python_install ['method' ] != PIP :
784
+ self .error (
785
+ extra_req_key ,
786
+ 'You need to install your project with pip '
787
+ 'to use extra_requirements' ,
788
+ code = PYTHON_INVALID ,
789
+ )
790
+ python_install ['extra_requirements' ] = extra_requirements
791
+ else :
792
+ self .error (
793
+ key ,
794
+ '"path" or "requirements" key is required' ,
795
+ code = CONFIG_REQUIRED ,
796
+ )
797
+ return python_install
798
+
718
799
def get_valid_python_versions (self ):
719
800
"""
720
801
Get the valid python versions for the current docker image.
@@ -951,7 +1032,22 @@ def build(self):
951
1032
952
1033
@property
953
1034
def python (self ):
954
- return Python (** self ._config ['python' ])
1035
+ python_install = []
1036
+ python = self ._config ['python' ]
1037
+ for install in python ['install' ]:
1038
+ if 'requirements' in install :
1039
+ python_install .append (
1040
+ PythonInstallRequirements (** install )
1041
+ )
1042
+ elif 'path' in install :
1043
+ python_install .append (
1044
+ PythonInstall (** install )
1045
+ )
1046
+ return Python (
1047
+ version = python ['version' ],
1048
+ install = python_install ,
1049
+ use_system_site_packages = python ['use_system_site_packages' ],
1050
+ )
955
1051
956
1052
@property
957
1053
def sphinx (self ):
0 commit comments