1
- # -*- coding: utf-8 -*-
2
-
3
1
# pylint: disable=too-many-lines
4
2
5
3
"""Build configuration for rtd."""
6
4
5
+ import copy
7
6
import os
8
7
from contextlib import contextmanager
9
8
10
9
from django .conf import settings
11
10
11
+ from readthedocs .config .utils import list_to_dict , to_dict
12
12
from readthedocs .projects .constants import DOCUMENTATION_CHOICES
13
13
14
14
from .find import find_one
15
- from .models import Build , Conda , Mkdocs , Python , Sphinx , Submodules
15
+ from .models import (
16
+ Build ,
17
+ Conda ,
18
+ Mkdocs ,
19
+ Python ,
20
+ PythonInstall ,
21
+ PythonInstallRequirements ,
22
+ Sphinx ,
23
+ Submodules ,
24
+ )
16
25
from .parser import ParseError , parse
17
26
from .validation import (
18
27
VALUE_NOT_FOUND ,
19
28
ValidationError ,
20
29
validate_bool ,
21
30
validate_choice ,
22
31
validate_dict ,
32
+ validate_directory ,
23
33
validate_file ,
24
34
validate_list ,
25
35
validate_string ,
34
44
'ConfigError' ,
35
45
'ConfigOptionNotSupportedError' ,
36
46
'InvalidConfig' ,
47
+ 'PIP' ,
48
+ 'SETUPTOOLS' ,
37
49
)
38
50
39
51
ALL = 'all'
52
+ PIP = 'pip'
53
+ SETUPTOOLS = 'setuptools'
40
54
CONFIG_FILENAME_REGEX = r'^\.?readthedocs.ya?ml$'
41
55
42
56
CONFIG_NOT_SUPPORTED = 'config-not-supported'
@@ -136,7 +150,7 @@ class BuildConfigBase:
136
150
137
151
def __init__ (self , env_config , raw_config , source_file ):
138
152
self .env_config = env_config
139
- self .raw_config = raw_config
153
+ self .raw_config = copy . deepcopy ( raw_config )
140
154
self .source_file = source_file
141
155
if os .path .isdir (self .source_file ):
142
156
self .base_path = self .source_file
@@ -236,10 +250,7 @@ def as_dict(self):
236
250
config = {}
237
251
for name in self .PUBLIC_ATTRIBUTES :
238
252
attr = getattr (self , name )
239
- if hasattr (attr , '_asdict' ):
240
- config [name ] = attr ._asdict ()
241
- else :
242
- config [name ] = attr
253
+ config [name ] = to_dict (attr )
243
254
return config
244
255
245
256
def __getattr__ (self , name ):
@@ -456,7 +467,9 @@ def validate_requirements_file(self):
456
467
if not requirements_file :
457
468
return None
458
469
with self .catch_validation_error ('requirements_file' ):
459
- validate_file (requirements_file , self .base_path )
470
+ requirements_file = validate_file (
471
+ requirements_file , self .base_path
472
+ )
460
473
return requirements_file
461
474
462
475
def validate_formats (self ):
@@ -482,9 +495,39 @@ def formats(self):
482
495
@property
483
496
def python (self ):
484
497
"""Python related configuration."""
498
+ python = self ._config ['python' ]
485
499
requirements = self ._config ['requirements_file' ]
486
- self ._config ['python' ]['requirements' ] = requirements
487
- return Python (** self ._config ['python' ])
500
+ python_install = []
501
+
502
+ # Always append a `PythonInstallRequirements` option.
503
+ # If requirements is None, rtd will try to find a requirements file.
504
+ python_install .append (
505
+ PythonInstallRequirements (
506
+ requirements = requirements ,
507
+ )
508
+ )
509
+ if python ['install_with_pip' ]:
510
+ python_install .append (
511
+ PythonInstall (
512
+ path = self .base_path ,
513
+ method = PIP ,
514
+ extra_requirements = python ['extra_requirements' ],
515
+ )
516
+ )
517
+ elif python ['install_with_setup' ]:
518
+ python_install .append (
519
+ PythonInstall (
520
+ path = self .base_path ,
521
+ method = SETUPTOOLS ,
522
+ extra_requirements = [],
523
+ )
524
+ )
525
+
526
+ return Python (
527
+ version = python ['version' ],
528
+ install = python_install ,
529
+ use_system_site_packages = python ['use_system_site_packages' ],
530
+ )
488
531
489
532
@property
490
533
def conda (self ):
@@ -536,7 +579,7 @@ class BuildConfigV2(BuildConfigBase):
536
579
valid_formats = ['htmlzip' , 'pdf' , 'epub' ]
537
580
valid_build_images = ['1.0' , '2.0' , '3.0' , 'stable' , 'latest' ]
538
581
default_build_image = 'latest'
539
- valid_install_options = ['pip' , 'setup.py' ]
582
+ valid_install_method = [PIP , SETUPTOOLS ]
540
583
valid_sphinx_builders = {
541
584
'html' : 'sphinx' ,
542
585
'htmldir' : 'sphinx_htmldir' ,
@@ -659,39 +702,22 @@ def validate_python(self):
659
702
self .get_valid_python_versions (),
660
703
)
661
704
662
- with self .catch_validation_error ('python.requirements' ):
663
- requirements = self .defaults .get ('requirements_file' )
664
- requirements = self .pop_config ('python.requirements' , requirements )
665
- if requirements != '' and requirements is not None :
666
- requirements = validate_file (requirements , self .base_path )
667
- python ['requirements' ] = requirements
668
-
669
705
with self .catch_validation_error ('python.install' ):
670
- install = (
671
- 'setup.py' if self .defaults .get ('install_project' ) else None
672
- )
673
- install = self .pop_config ('python.install' , install )
674
- if install is not None :
675
- validate_choice (install , self .valid_install_options )
676
- python ['install_with_setup' ] = install == 'setup.py'
677
- python ['install_with_pip' ] = install == 'pip'
678
-
679
- with self .catch_validation_error ('python.extra_requirements' ):
680
- extra_requirements = self .pop_config (
681
- 'python.extra_requirements' ,
682
- [],
683
- )
684
- extra_requirements = validate_list (extra_requirements )
685
- if extra_requirements and not python ['install_with_pip' ]:
686
- self .error (
687
- 'python.extra_requirements' ,
688
- 'You need to install your project with pip '
689
- 'to use extra_requirements' ,
690
- code = PYTHON_INVALID ,
706
+ raw_install = self .raw_config .get ('python' , {}).get ('install' , [])
707
+ validate_list (raw_install )
708
+ if raw_install :
709
+ # Transform to a dict, so it's easy to validate extra keys.
710
+ self .raw_config .setdefault ('python' , {})['install' ] = (
711
+ list_to_dict (raw_install )
691
712
)
692
- python ['extra_requirements' ] = [
693
- validate_string (extra ) for extra in extra_requirements
694
- ]
713
+ else :
714
+ self .pop_config ('python.install' )
715
+
716
+ raw_install = self .raw_config .get ('python' , {}).get ('install' , [])
717
+ python ['install' ] = [
718
+ self .validate_python_install (index )
719
+ for index in range (len (raw_install ))
720
+ ]
695
721
696
722
with self .catch_validation_error ('python.system_packages' ):
697
723
system_packages = self .defaults .get (
@@ -706,6 +732,60 @@ def validate_python(self):
706
732
707
733
return python
708
734
735
+ def validate_python_install (self , index ):
736
+ """Validates the python.install.{index} key."""
737
+ python_install = {}
738
+ key = 'python.install.{}' .format (index )
739
+ raw_install = self .raw_config ['python' ]['install' ][str (index )]
740
+ with self .catch_validation_error (key ):
741
+ validate_dict (raw_install )
742
+
743
+ if 'requirements' in raw_install :
744
+ requirements_key = key + '.requirements'
745
+ with self .catch_validation_error (requirements_key ):
746
+ requirements = validate_file (
747
+ self .pop_config (requirements_key ),
748
+ self .base_path
749
+ )
750
+ python_install ['requirements' ] = requirements
751
+ elif 'path' in raw_install :
752
+ path_key = key + '.path'
753
+ with self .catch_validation_error (path_key ):
754
+ path = validate_directory (
755
+ self .pop_config (path_key ),
756
+ self .base_path
757
+ )
758
+ python_install ['path' ] = path
759
+
760
+ method_key = key + '.method'
761
+ with self .catch_validation_error (method_key ):
762
+ method = validate_choice (
763
+ self .pop_config (method_key , PIP ),
764
+ self .valid_install_method
765
+ )
766
+ python_install ['method' ] = method
767
+
768
+ extra_req_key = key + '.extra_requirements'
769
+ with self .catch_validation_error (extra_req_key ):
770
+ extra_requirements = validate_list (
771
+ self .pop_config (extra_req_key , [])
772
+ )
773
+ if extra_requirements and python_install ['method' ] != PIP :
774
+ self .error (
775
+ extra_req_key ,
776
+ 'You need to install your project with pip '
777
+ 'to use extra_requirements' ,
778
+ code = PYTHON_INVALID ,
779
+ )
780
+ python_install ['extra_requirements' ] = extra_requirements
781
+ else :
782
+ self .error (
783
+ key ,
784
+ '"path" or "requirements" key is required' ,
785
+ code = CONFIG_REQUIRED ,
786
+ )
787
+ return python_install
788
+
709
789
def get_valid_python_versions (self ):
710
790
"""
711
791
Get the valid python versions for the current docker image.
@@ -942,7 +1022,22 @@ def build(self):
942
1022
943
1023
@property
944
1024
def python (self ):
945
- return Python (** self ._config ['python' ])
1025
+ python_install = []
1026
+ python = self ._config ['python' ]
1027
+ for install in python ['install' ]:
1028
+ if 'requirements' in install :
1029
+ python_install .append (
1030
+ PythonInstallRequirements (** install )
1031
+ )
1032
+ elif 'path' in install :
1033
+ python_install .append (
1034
+ PythonInstall (** install )
1035
+ )
1036
+ return Python (
1037
+ version = python ['version' ],
1038
+ install = python_install ,
1039
+ use_system_site_packages = python ['use_system_site_packages' ],
1040
+ )
946
1041
947
1042
@property
948
1043
def sphinx (self ):
0 commit comments