17
17
from .models import Build , Conda , Mkdocs , Python , Sphinx , Submodules
18
18
from .parser import ParseError , parse
19
19
from .validation import (
20
+ VALUE_NOT_FOUND ,
20
21
ValidationError ,
21
22
validate_bool ,
22
23
validate_choice ,
25
26
validate_file ,
26
27
validate_list ,
27
28
validate_string ,
28
- validate_value_exists ,
29
29
)
30
30
31
31
__all__ = (
54
54
PYTHON_INVALID = 'python-invalid'
55
55
SUBMODULES_INVALID = 'submodules-invalid'
56
56
INVALID_KEYS_COMBINATION = 'invalid-keys-combination'
57
+ INVALID_KEY = 'invalid-key'
57
58
58
59
DOCKER_DEFAULT_IMAGE = 'readthedocs/build'
59
60
DOCKER_DEFAULT_VERSION = '2.0'
@@ -176,6 +177,41 @@ def catch_validation_error(self, key):
176
177
source_position = self .source_position ,
177
178
)
178
179
180
+ def pop (self , name , container , default , raise_ex ):
181
+ """
182
+ Search and pop a key inside a dict.
183
+
184
+ This will pop the keys recursively if the container is empty.
185
+
186
+ :param name: the key name in a list form (``['key', 'inner']``)
187
+ :param container: a dictionary that contains the key
188
+ :param default: default value to return if the key doesn't exists
189
+ :param raise_ex: if True, raises an exception when a key is not found
190
+ """
191
+ key = name [0 ]
192
+ validate_dict (container )
193
+ if key in container :
194
+ if len (name ) > 1 :
195
+ value = self .pop (name [1 :], container [key ], default , raise_ex )
196
+ if not container [key ]:
197
+ container .pop (key )
198
+ else :
199
+ value = container .pop (key )
200
+ return value
201
+ if raise_ex :
202
+ raise ValidationError (key , VALUE_NOT_FOUND )
203
+ return default
204
+
205
+ def pop_config (self , key , default = None , raise_ex = False ):
206
+ """
207
+ Search and pop a key (recursively) from `self.raw_config`.
208
+
209
+ :param key: the key name in a dotted form (``key.innerkey``)
210
+ :param default: Optionally, it can receive a default value
211
+ :param raise_ex: If True, raises an exception when the key is not found
212
+ """
213
+ return self .pop (key .split ('.' ), self .raw_config , default , raise_ex )
214
+
179
215
def validate (self ):
180
216
raise NotImplementedError ()
181
217
@@ -594,6 +630,7 @@ def validate(self):
594
630
# TODO: remove later
595
631
self .validate_final_doc_type ()
596
632
self ._config ['submodules' ] = self .validate_submodules ()
633
+ self .validate_keys ()
597
634
598
635
def validate_formats (self ):
599
636
"""
@@ -602,7 +639,7 @@ def validate_formats(self):
602
639
The ``ALL`` keyword can be used to indicate that all formats are used.
603
640
We ignore the default values here.
604
641
"""
605
- formats = self .raw_config . get ('formats' , [])
642
+ formats = self .pop_config ('formats' , [])
606
643
if formats == ALL :
607
644
return self .valid_formats
608
645
with self .catch_validation_error ('formats' ):
@@ -622,7 +659,7 @@ def validate_conda(self):
622
659
623
660
conda = {}
624
661
with self .catch_validation_error ('conda.environment' ):
625
- environment = validate_value_exists ( ' environment' , raw_conda )
662
+ environment = self . pop_config ( 'conda. environment' , raise_ex = True )
626
663
conda ['environment' ] = validate_file (environment , self .base_path )
627
664
return conda
628
665
@@ -637,7 +674,7 @@ def validate_build(self):
637
674
validate_dict (raw_build )
638
675
build = {}
639
676
with self .catch_validation_error ('build.image' ):
640
- image = raw_build . get ( ' image' , self .default_build_image )
677
+ image = self . pop_config ( 'build. image' , self .default_build_image )
641
678
build ['image' ] = '{}:{}' .format (
642
679
DOCKER_DEFAULT_IMAGE ,
643
680
validate_choice (
@@ -674,7 +711,7 @@ def validate_python(self):
674
711
675
712
python = {}
676
713
with self .catch_validation_error ('python.version' ):
677
- version = raw_python . get ( ' version' , 3 )
714
+ version = self . pop_config ( 'python. version' , 3 )
678
715
if isinstance (version , six .string_types ):
679
716
try :
680
717
version = int (version )
@@ -690,7 +727,7 @@ def validate_python(self):
690
727
691
728
with self .catch_validation_error ('python.requirements' ):
692
729
requirements = self .defaults .get ('requirements_file' )
693
- requirements = raw_python . get ( ' requirements' , requirements )
730
+ requirements = self . pop_config ( 'python. requirements' , requirements )
694
731
if requirements != '' and requirements is not None :
695
732
requirements = validate_file (requirements , self .base_path )
696
733
python ['requirements' ] = requirements
@@ -699,14 +736,16 @@ def validate_python(self):
699
736
install = (
700
737
'setup.py' if self .defaults .get ('install_project' ) else None
701
738
)
702
- install = raw_python . get ( ' install' , install )
739
+ install = self . pop_config ( 'python. install' , install )
703
740
if install is not None :
704
741
validate_choice (install , self .valid_install_options )
705
742
python ['install_with_setup' ] = install == 'setup.py'
706
743
python ['install_with_pip' ] = install == 'pip'
707
744
708
745
with self .catch_validation_error ('python.extra_requirements' ):
709
- extra_requirements = raw_python .get ('extra_requirements' , [])
746
+ extra_requirements = self .pop_config (
747
+ 'python.extra_requirements' , []
748
+ )
710
749
extra_requirements = validate_list (extra_requirements )
711
750
if extra_requirements and not python ['install_with_pip' ]:
712
751
self .error (
@@ -724,8 +763,8 @@ def validate_python(self):
724
763
'use_system_packages' ,
725
764
False ,
726
765
)
727
- system_packages = raw_python . get (
728
- 'system_packages' ,
766
+ system_packages = self . pop_config (
767
+ 'python. system_packages' ,
729
768
system_packages ,
730
769
)
731
770
python ['use_system_site_packages' ] = validate_bool (system_packages )
@@ -778,13 +817,13 @@ def validate_mkdocs(self):
778
817
779
818
mkdocs = {}
780
819
with self .catch_validation_error ('mkdocs.configuration' ):
781
- configuration = raw_mkdocs . get ( ' configuration' )
820
+ configuration = self . pop_config ( 'mkdocs. configuration', None )
782
821
if configuration is not None :
783
822
configuration = validate_file (configuration , self .base_path )
784
823
mkdocs ['configuration' ] = configuration
785
824
786
825
with self .catch_validation_error ('mkdocs.fail_on_warning' ):
787
- fail_on_warning = raw_mkdocs . get ( ' fail_on_warning' , False )
826
+ fail_on_warning = self . pop_config ( 'mkdocs. fail_on_warning' , False )
788
827
mkdocs ['fail_on_warning' ] = validate_bool (fail_on_warning )
789
828
790
829
return mkdocs
@@ -812,7 +851,7 @@ def validate_sphinx(self):
812
851
sphinx = {}
813
852
with self .catch_validation_error ('sphinx.builder' ):
814
853
builder = validate_choice (
815
- raw_sphinx . get ( ' builder' , 'html' ),
854
+ self . pop_config ( 'sphinx. builder' , 'html' ),
816
855
self .valid_sphinx_builders .keys (),
817
856
)
818
857
sphinx ['builder' ] = self .valid_sphinx_builders [builder ]
@@ -822,13 +861,15 @@ def validate_sphinx(self):
822
861
# The default value can be empty
823
862
if not configuration :
824
863
configuration = None
825
- configuration = raw_sphinx .get ('configuration' , configuration )
864
+ configuration = self .pop_config (
865
+ 'sphinx.configuration' , configuration
866
+ )
826
867
if configuration is not None :
827
868
configuration = validate_file (configuration , self .base_path )
828
869
sphinx ['configuration' ] = configuration
829
870
830
871
with self .catch_validation_error ('sphinx.fail_on_warning' ):
831
- fail_on_warning = raw_sphinx . get ( ' fail_on_warning' , False )
872
+ fail_on_warning = self . pop_config ( 'sphinx. fail_on_warning' , False )
832
873
sphinx ['fail_on_warning' ] = validate_bool (fail_on_warning )
833
874
834
875
return sphinx
@@ -870,7 +911,7 @@ def validate_submodules(self):
870
911
871
912
submodules = {}
872
913
with self .catch_validation_error ('submodules.include' ):
873
- include = raw_submodules . get ( ' include' , [])
914
+ include = self . pop_config ( 'submodules. include' , [])
874
915
if include != ALL :
875
916
include = [
876
917
validate_string (submodule )
@@ -880,7 +921,7 @@ def validate_submodules(self):
880
921
881
922
with self .catch_validation_error ('submodules.exclude' ):
882
923
default = [] if submodules ['include' ] else ALL
883
- exclude = raw_submodules . get ( ' exclude' , default )
924
+ exclude = self . pop_config ( 'submodules. exclude' , default )
884
925
if exclude != ALL :
885
926
exclude = [
886
927
validate_string (submodule )
@@ -902,11 +943,54 @@ def validate_submodules(self):
902
943
)
903
944
904
945
with self .catch_validation_error ('submodules.recursive' ):
905
- recursive = raw_submodules . get ( ' recursive' , False )
946
+ recursive = self . pop_config ( 'submodules. recursive' , False )
906
947
submodules ['recursive' ] = validate_bool (recursive )
907
948
908
949
return submodules
909
950
951
+ def validate_keys (self ):
952
+ """
953
+ Checks that we don't have extra keys (invalid ones).
954
+
955
+ This should be called after all the validations are done
956
+ and all keys are popped from `self.raw_config`.
957
+ """
958
+ msg = (
959
+ 'Invalid configuration option: {}. '
960
+ 'Make sure the key name is correct.'
961
+ )
962
+ # The version key isn't popped, but it's
963
+ # validated in `load`.
964
+ self .pop_config ('version' , None )
965
+ wrong_key = '.' .join (self ._get_extra_key (self .raw_config ))
966
+ if wrong_key :
967
+ self .error (
968
+ wrong_key ,
969
+ msg .format (wrong_key ),
970
+ code = INVALID_KEY ,
971
+ )
972
+
973
+ def _get_extra_key (self , value ):
974
+ """
975
+ Get the extra keyname (list form) of a dict object.
976
+
977
+ If there is more than one extra key, the first one is returned.
978
+
979
+ Example::
980
+
981
+ {
982
+ 'key': {
983
+ 'name': 'inner',
984
+ }
985
+ }
986
+
987
+ Will return `['key', 'name']`.
988
+ """
989
+ if isinstance (value , dict ) and value :
990
+ key_name = next (iter (value ))
991
+ return [key_name ] + self ._get_extra_key (value [key_name ])
992
+ return []
993
+
910
994
@property
911
995
def formats (self ):
912
996
return self ._config ['formats' ]
0 commit comments