-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
Refactor configuration object to class based #4298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 12 commits
8e6a73c
10e750c
0c7fdd8
3243d12
7799aa3
928dd7f
4c76ce4
bf055ca
37c67dd
31076bf
81d0df9
baceda6
ea4b0fb
666c952
a6c18d5
5928a37
5d5c44a
dcee59c
3c7bd9f
fefe192
33abd0d
b49d0a2
097b8fe
61756c3
e377829
cfb0b1d
4cbc3c3
985ff7a
d0e9f78
d48996d
2860205
2509235
7f360b0
259082f
6e8f723
110ae38
ae20ca8
23af026
668cb27
3604d50
c728680
6edf6f0
1b2014a
c19f0e0
1964317
fdad93d
89e0dcf
215206e
5c66206
62a3d98
711b595
b43372b
e85e587
376962e
8d8197a
811bf72
241bbed
cdce197
6ef45fe
bc67350
8c516ba
84bb4bc
b8bba7e
b0bf8c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -76,53 +76,39 @@ def __init__(self, key, code, error_message, source_file=None, | |
super(InvalidConfig, self).__init__(message, code=code) | ||
|
||
|
||
class BuildConfig(dict): | ||
class BuildConfigBase(object): | ||
|
||
""" | ||
Config that handles the build of one particular documentation. | ||
|
||
Config keys can be accessed with a dictionary lookup:: | ||
|
||
>>> build_config['type'] | ||
'sphinx' | ||
|
||
You need to call ``validate`` before the config is ready to use. Also | ||
setting the ``output_base`` is required before using it for a build. | ||
You need to call ``validate`` before the config is ready to use. | ||
Also setting the ``output_base`` is required before using it for a build. | ||
""" | ||
|
||
BASE_INVALID_MESSAGE = 'Invalid value for base: {base}' | ||
BASE_NOT_A_DIR_MESSAGE = '"base" is not a directory: {base}' | ||
NAME_REQUIRED_MESSAGE = 'Missing key "name"' | ||
NAME_INVALID_MESSAGE = ( | ||
'Invalid name "{name}". Valid values must match {name_re}') | ||
TYPE_REQUIRED_MESSAGE = 'Missing key "type"' | ||
CONF_FILE_REQUIRED_MESSAGE = 'Missing key "conf_file"' | ||
PYTHON_INVALID_MESSAGE = '"python" section must be a mapping.' | ||
PYTHON_EXTRA_REQUIREMENTS_INVALID_MESSAGE = ( | ||
'"python.extra_requirements" section must be a list.') | ||
|
||
PYTHON_SUPPORTED_VERSIONS = [2, 2.7, 3, 3.5] | ||
DOCKER_SUPPORTED_VERSIONS = ['1.0', '2.0', 'latest'] | ||
|
||
def __init__(self, env_config, raw_config, source_file, source_position): | ||
self.env_config = env_config | ||
self.raw_config = raw_config | ||
self.source_file = source_file | ||
self.source_position = source_position | ||
super(BuildConfig, self).__init__() | ||
super(BuildConfigBase, self).__init__() | ||
|
||
def error(self, key, message, code): | ||
"""Raise an error related to ``key``.""" | ||
source = '{file} [{pos}]'.format( | ||
file=self.source_file, | ||
pos=self.source_position) | ||
pos=self.source_position | ||
) | ||
error_message = '{source}: {message}'.format( | ||
source=source, | ||
message=message | ||
) | ||
raise InvalidConfig( | ||
key=key, | ||
code=code, | ||
error_message='{source}: {message}'.format(source=source, | ||
message=message), | ||
error_message=error_message, | ||
source_file=self.source_file, | ||
source_position=self.source_position) | ||
source_position=self.source_position | ||
) | ||
|
||
@contextmanager | ||
def catch_validation_error(self, key): | ||
|
@@ -135,7 +121,106 @@ def catch_validation_error(self, key): | |
code=error.code, | ||
error_message=str(error), | ||
source_file=self.source_file, | ||
source_position=self.source_position) | ||
source_position=self.source_position | ||
) | ||
|
||
def validate(self): | ||
raise NotImplementedError() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm just leaving this one, as we always need it. |
||
|
||
@property | ||
def base(self): | ||
raise NotImplementedError() | ||
|
||
def output_base(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def name(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def version(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def type(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def pip_install(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def install_project(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def extra_requirements(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def python_interpreter(self): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe we can move this to a function outside the config object There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps it makes somewhere else, yeah. We can probably leave for just now. Feel free to open an issue of you want to track this work. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I opened #4343 |
||
raise NotImplementedError() | ||
|
||
@property | ||
def python_version(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def python_full_version(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def use_system_site_packages(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def use_conda(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def conda_file(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def requirements_file(self): | ||
raise NotImplementedError() | ||
|
||
@property | ||
def formats(self): | ||
"""Documentation formats to be built.""" | ||
raise NotImplementedError() | ||
|
||
@property | ||
def build(self): | ||
raise NotImplementedError() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An improvement to this pattern would be to remove the need for all of this duplicated code on the base class. I guess the general advice with python is to use duck typing instead of a stricter abstract class pattern. One option is to use a meta class on the base, which would raise NotImplemented on any missing methods. Another option would be to raise NotImplemented, or a more specific error even, from the v1 or v2 class directly, and avoid requiring properties on the base class for each new field. Do you think it would be better to remove all of these properties on the base class? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is already implemented https://github.com/rtfd/readthedocs.org/pull/4298/files#diff-2b6854693f39acfa183fca7fef27dcfbR148. I think you are seeing an old diff 🤔 There aren't more properties in the base class There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yup, github sneaked old feedback in. Disregard. |
||
|
||
|
||
class BuildConfig(BuildConfigBase, dict): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm keeping the dict thing here for a while, so the refactor isn't too big |
||
|
||
""" | ||
Version 1 of the configuration file. | ||
|
||
Config keys can be accessed with a dictionary lookup:: | ||
|
||
>>> build_config['type'] | ||
'sphinx' | ||
""" | ||
|
||
BASE_INVALID_MESSAGE = 'Invalid value for base: {base}' | ||
BASE_NOT_A_DIR_MESSAGE = '"base" is not a directory: {base}' | ||
NAME_REQUIRED_MESSAGE = 'Missing key "name"' | ||
NAME_INVALID_MESSAGE = ( | ||
'Invalid name "{name}". Valid values must match {name_re}') | ||
TYPE_REQUIRED_MESSAGE = 'Missing key "type"' | ||
CONF_FILE_REQUIRED_MESSAGE = 'Missing key "conf_file"' | ||
PYTHON_INVALID_MESSAGE = '"python" section must be a mapping.' | ||
PYTHON_EXTRA_REQUIREMENTS_INVALID_MESSAGE = ( | ||
'"python.extra_requirements" section must be a list.') | ||
|
||
PYTHON_SUPPORTED_VERSIONS = [2, 2.7, 3, 3.5] | ||
DOCKER_SUPPORTED_VERSIONS = ['1.0', '2.0', 'latest'] | ||
|
||
|
||
def get_valid_types(self): # noqa | ||
return ( | ||
|
@@ -432,6 +517,92 @@ def validate_formats(self): | |
|
||
return True | ||
|
||
@property | ||
def version(self): # noqa | ||
return '1' | ||
|
||
@property | ||
def name(self): # noqa | ||
return self['name'] | ||
|
||
@property | ||
def base(self): # noqa | ||
return self['base'] | ||
|
||
@property | ||
def output_base(self): # noqa | ||
return self['output_base'] | ||
|
||
@property | ||
def type(self): # noqa | ||
return self['type'] | ||
|
||
@property | ||
def formats(self): # noqa | ||
return self['formats'] | ||
|
||
@property | ||
def python(self): # noqa | ||
return self['python'] | ||
|
||
@property | ||
def build(self): # noqa | ||
return self['build'] | ||
|
||
@property | ||
def pip_install(self): # noqa | ||
return self['python']['pip_install'] | ||
|
||
@property | ||
def install_project(self): # noqa | ||
return None | ||
|
||
@property | ||
def extra_requirements(self): # noqa | ||
return self['python']['extra_requirements'] | ||
|
||
@property | ||
def python_interpreter(self): # noqa | ||
ver = self.python_full_version | ||
return 'python{0}'.format(ver) | ||
|
||
@property | ||
def python_version(self): # noqa | ||
return self['python']['version'] | ||
|
||
@property | ||
def python_full_version(self): # noqa | ||
ver = self.python_version | ||
if ver in [2, 3]: | ||
# Get the highest version of the major series version if user only | ||
# gave us a version of '2', or '3' | ||
ver = max( | ||
version | ||
for version in self.get_valid_python_versions() | ||
if version < ver + 1 | ||
) | ||
return ver | ||
|
||
@property | ||
def use_system_site_packages(self): # noqa | ||
return self['python']['use_system_site_packages'] | ||
|
||
@property | ||
def use_conda(self): # noqa | ||
return 'conda' in self | ||
|
||
@property | ||
def conda_file(self): # noqa | ||
return self['conda']['file'] | ||
|
||
@property | ||
def requirements_file(self): # noqa | ||
return self['requirements_file'] | ||
|
||
@property | ||
def build_image(self): # noqa | ||
return self['build']['image'] | ||
|
||
|
||
class ProjectConfig(list): | ||
|
||
|
@@ -442,11 +613,6 @@ def validate(self): | |
for build in self: | ||
build.validate() | ||
|
||
def set_output_base(self, directory): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We weren't using this, and it's the only setter. We should keep all the config object as read-only I think. |
||
"""Set a common ``output_base`` for each configuration build.""" | ||
for build in self: | ||
build['output_base'] = os.path.abspath(directory) | ||
|
||
|
||
def load(path, env_config): | ||
""" | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
File position is generally really useful, we should make sure the file position is raised to users.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is
source
has thesource_position
:). The diff is a little messy and gives the impression to be removed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oops, missed that, yeah this is fine