diff --git a/prospector.yml b/prospector.yml index 64be6c1142a..f6101a32273 100644 --- a/prospector.yml +++ b/prospector.yml @@ -35,6 +35,15 @@ pep257: - D105 - D211 - D104 + - D212 # Multi-line docstring summary should start at the first line + - D107 # Missing docstring in __init__ + - D106 # Missing docstring in public nested class + + # pydocstyle + - D406 # Section name should end with a newline ('Examples', not 'Examples::') + - D407 # Missing dashed underline after section ('Examples') + - D412 # No blank lines allowed between a section header and its content ('Examples') + - D413 # Missing blank line after last section ('Examples') pyflakes: disable: diff --git a/readthedocs/builds/managers.py b/readthedocs/builds/managers.py index 716dad6dde6..8b4deda7475 100644 --- a/readthedocs/builds/managers.py +++ b/readthedocs/builds/managers.py @@ -16,7 +16,8 @@ class VersionManagerBase(models.Manager): - """Version manager for manager only queries + """ + Version manager for manager only queries. For queries not suitable for the :py:cls:`VersionQuerySet`, such as create queries. diff --git a/readthedocs/builds/syncers.py b/readthedocs/builds/syncers.py index 0ff47e08a0f..66ebca0c4ab 100644 --- a/readthedocs/builds/syncers.py +++ b/readthedocs/builds/syncers.py @@ -1,7 +1,8 @@ -"""Classes to copy files between build and web servers +""" +Classes to copy files between build and web servers. -"Syncers" copy files from the local machine, while "Pullers" copy files to -the local machine. +"Syncers" copy files from the local machine, while "Pullers" copy files to the +local machine. """ from __future__ import absolute_import diff --git a/readthedocs/builds/version_slug.py b/readthedocs/builds/version_slug.py index 4a77bcb3217..581a8db0bcb 100644 --- a/readthedocs/builds/version_slug.py +++ b/readthedocs/builds/version_slug.py @@ -1,4 +1,5 @@ -"""Contains logic for handling version slugs. +""" +Contains logic for handling version slugs. Handling slugs for versions is not too straightforward. We need to allow some characters which are uncommon in usual slugs. They are dots and underscores. @@ -32,8 +33,8 @@ def get_fields_with_model(cls): """ Replace deprecated function of the same name in Model._meta. - This replaces deprecated function (as of Django 1.10) in - Model._meta as prescrived in the Django docs. + This replaces deprecated function (as of Django 1.10) in Model._meta as + prescrived in the Django docs. https://docs.djangoproject.com/en/1.11/ref/models/meta/#migrating-from-the-old-api """ return [ diff --git a/readthedocs/core/admin.py b/readthedocs/core/admin.py index 6b6b533c332..5dbc3335dd9 100644 --- a/readthedocs/core/admin.py +++ b/readthedocs/core/admin.py @@ -1,4 +1,4 @@ -"""Django admin interface for core models""" +"""Django admin interface for core models.""" from __future__ import absolute_import from datetime import datetime, timedelta @@ -22,7 +22,7 @@ class UserProjectInline(admin.TabularInline): class UserProjectFilter(admin.SimpleListFilter): - """Filter users based on project properties""" + """Filter users based on project properties.""" parameter_name = 'project_state' title = _('user projects') @@ -39,7 +39,8 @@ def lookups(self, request, model_admin): ) def queryset(self, request, queryset): - """Add filters to queryset filter + """ + Add filters to queryset filter. ``PROJECT_ACTIVE`` and ``PROJECT_BUILT`` look for versions on projects, ``PROJECT_RECENT`` looks for projects with builds in the last year diff --git a/readthedocs/core/middleware.py b/readthedocs/core/middleware.py index 43ff44f97fc..a6034ccd047 100644 --- a/readthedocs/core/middleware.py +++ b/readthedocs/core/middleware.py @@ -31,10 +31,11 @@ class SubdomainMiddleware(object): - """Middleware to display docs for non-dashboard domains""" + """Middleware to display docs for non-dashboard domains.""" def process_request(self, request): - """Process requests for unhandled domains + """ + Process requests for unhandled domains. If the request is not for our ``PUBLIC_DOMAIN``, or if ``PUBLIC_DOMAIN`` is not set and the request is for a subdomain on ``PRODUCTION_DOMAIN``, @@ -132,22 +133,22 @@ def process_request(self, request): class SingleVersionMiddleware(object): - """Reset urlconf for requests for 'single_version' docs. - - In settings.MIDDLEWARE_CLASSES, SingleVersionMiddleware must follow - after SubdomainMiddleware. + """ + Reset urlconf for requests for 'single_version' docs. + In settings.MIDDLEWARE_CLASSES, SingleVersionMiddleware must follow after + SubdomainMiddleware. """ def _get_slug(self, request): - """Get slug from URLs requesting docs. + """ + Get slug from URLs requesting docs. If URL is like '/docs//', we split path and pull out slug. If URL is subdomain or CNAME, we simply read request.slug, which is set by SubdomainMiddleware. - """ slug = None if hasattr(request, 'slug'): @@ -187,16 +188,16 @@ def process_request(self, request): class ProxyMiddleware(object): """ - Middleware that sets REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, if the + Middleware that sets REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, if the. latter is set. This is useful if you're sitting behind a reverse proxy that - causes each request's REMOTE_ADDR to be set to 127.0.0.1. - Note that this does NOT validate HTTP_X_FORWARDED_FOR. If you're not behind - a reverse proxy that sets HTTP_X_FORWARDED_FOR automatically, do not use - this middleware. Anybody can spoof the value of HTTP_X_FORWARDED_FOR, and - because this sets REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, that means - anybody can "fake" their IP address. Only use this when you can absolutely - trust the value of HTTP_X_FORWARDED_FOR. + causes each request's REMOTE_ADDR to be set to 127.0.0.1. Note that this + does NOT validate HTTP_X_FORWARDED_FOR. If you're not behind a reverse proxy + that sets HTTP_X_FORWARDED_FOR automatically, do not use this middleware. + Anybody can spoof the value of HTTP_X_FORWARDED_FOR, and because this sets + REMOTE_ADDR based on HTTP_X_FORWARDED_FOR, that means anybody can "fake" + their IP address. Only use this when you can absolutely trust the value of + HTTP_X_FORWARDED_FOR. """ def process_request(self, request): diff --git a/readthedocs/core/resolver.py b/readthedocs/core/resolver.py index c133ccd64f4..9abf9542bbc 100644 --- a/readthedocs/core/resolver.py +++ b/readthedocs/core/resolver.py @@ -1,4 +1,4 @@ -"""URL resolver for documentation""" +"""URL resolver for documentation.""" from __future__ import absolute_import from builtins import object @@ -54,7 +54,7 @@ class ResolverBase(object): def base_resolve_path(self, project_slug, filename, version_slug=None, language=None, private=False, single_version=None, subproject_slug=None, subdomain=None, cname=None): - """Resolve a with nothing smart, just filling in the blanks""" + """Resolve a with nothing smart, just filling in the blanks.""" # Only support `/docs/project' URLs outside our normal environment. Normally # the path should always have a subdomain or CNAME domain # pylint: disable=unused-argument @@ -80,7 +80,7 @@ def base_resolve_path(self, project_slug, filename, version_slug=None, def resolve_path(self, project, filename='', version_slug=None, language=None, single_version=None, subdomain=None, cname=None, private=None): - """Resolve a URL with a subset of fields defined""" + """Resolve a URL with a subset of fields defined.""" relation = project.superprojects.first() cname = cname or project.domains.filter(canonical=True).first() main_language_project = project.main_language_project @@ -145,7 +145,8 @@ def resolve(self, project, protocol='http', filename='', private=None, ) def _get_canonical_project(self, project): - """Get canonical project in the case of subproject or translations + """ + Get canonical project in the case of subproject or translations. :type project: Project :rtype: Project @@ -159,7 +160,7 @@ def _get_canonical_project(self, project): return project def _get_project_subdomain(self, project): - """Determine canonical project domain as subdomain""" + """Determine canonical project domain as subdomain.""" public_domain = getattr(settings, 'PUBLIC_DOMAIN', None) if self._use_subdomain(): project = self._get_canonical_project(project) @@ -177,9 +178,10 @@ def _get_private(self, project, version_slug): def _fix_filename(self, project, filename): """ - Force filenames that might be HTML file paths into proper URL's + Force filenames that might be HTML file paths into proper URL's. - This basically means stripping / and .html endings and then re-adding them properly. + This basically means stripping / and .html endings and then re-adding + them properly. """ # Bail out on non-html files if '.' in filename and '.html' not in filename: @@ -203,7 +205,7 @@ def _fix_filename(self, project, filename): return path def _use_subdomain(self): - """Make decision about whether to use a subdomain to serve docs""" + """Make decision about whether to use a subdomain to serve docs.""" use_subdomain = getattr(settings, 'USE_SUBDOMAIN', False) public_domain = getattr(settings, 'PUBLIC_DOMAIN', None) return use_subdomain and public_domain is not None diff --git a/readthedocs/core/settings.py b/readthedocs/core/settings.py index 8ec10af117a..d66c6d02d28 100644 --- a/readthedocs/core/settings.py +++ b/readthedocs/core/settings.py @@ -1,4 +1,4 @@ -"""Class based settings for complex settings inheritance""" +"""Class based settings for complex settings inheritance.""" from __future__ import absolute_import from builtins import object @@ -8,11 +8,12 @@ class Settings(object): - """Class-based settings wrapper""" + """Class-based settings wrapper.""" @classmethod def load_settings(cls, module_name): - """Export class variables and properties to module namespace + """ + Export class variables and properties to module namespace. This will export and class variable that is all upper case and doesn't begin with ``_``. These members will be set as attributes on the module diff --git a/readthedocs/core/symlink.py b/readthedocs/core/symlink.py index fcb0af5c89d..a8e9c394495 100644 --- a/readthedocs/core/symlink.py +++ b/readthedocs/core/symlink.py @@ -50,7 +50,6 @@ ja/ fabric -> rtd-builds/fabric/en/latest/ # single version - """ from __future__ import absolute_import @@ -121,9 +120,8 @@ def run(self): """ Create proper symlinks in the right order. - Since we have a small nest of directories and symlinks, - the ordering of these calls matter, - so we provide this helper to make life easier. + Since we have a small nest of directories and symlinks, the ordering of + these calls matter, so we provide this helper to make life easier. """ # Outside of the web root self.symlink_cnames() @@ -138,7 +136,8 @@ def run(self): self.symlink_versions() def symlink_cnames(self, domain=None): - """Symlink project CNAME domains + """ + Symlink project CNAME domains. Link from HOME/$CNAME_ROOT/ -> HOME/$WEB_ROOT/ @@ -164,13 +163,14 @@ def symlink_cnames(self, domain=None): run(['ln', '-nsf', self.project.doc_path, project_cname_symlink]) def remove_symlink_cname(self, domain): - """Remove CNAME symlink""" + """Remove CNAME symlink.""" self._log(u"Removing symlink for CNAME {0}".format(domain.domain)) symlink = os.path.join(self.CNAME_ROOT, domain.domain) os.unlink(symlink) def symlink_subprojects(self): - """Symlink project subprojects + """ + Symlink project subprojects. Link from $WEB_ROOT/projects/ -> $WEB_ROOT/ @@ -213,7 +213,8 @@ def symlink_subprojects(self): os.unlink(os.path.join(self.subproject_root, subproj)) def symlink_translations(self): - """Symlink project translations + """ + Symlink project translations. Link from $WEB_ROOT/// -> $WEB_ROOT/// @@ -247,7 +248,8 @@ def symlink_translations(self): shutil.rmtree(to_delete) def symlink_single_version(self): - """Symlink project single version + """ + Symlink project single version. Link from $WEB_ROOT/ -> HOME/user_builds//rtd-builds/latest/ @@ -268,7 +270,8 @@ def symlink_single_version(self): run(['ln', '-nsf', docs_dir, symlink]) def symlink_versions(self): - """Symlink project's versions + """ + Symlink project's versions. Link from $WEB_ROOT//// -> HOME/user_builds//rtd-builds/ @@ -295,7 +298,7 @@ def symlink_versions(self): os.unlink(os.path.join(version_dir, old_ver)) def get_default_version(self): - """Look up project default version, return None if not found""" + """Look up project default version, return None if not found.""" default_version = self.project.get_default_version() try: return self.get_version_queryset().get(slug=default_version) diff --git a/readthedocs/core/tasks.py b/readthedocs/core/tasks.py index a44cc543d81..53c16d8f982 100644 --- a/readthedocs/core/tasks.py +++ b/readthedocs/core/tasks.py @@ -1,4 +1,4 @@ -"""Basic tasks""" +"""Basic tasks.""" from __future__ import absolute_import import logging @@ -19,7 +19,8 @@ @app.task(queue='web', time_limit=EMAIL_TIME_LIMIT) def send_email_task(recipient, subject, template, template_html, context=None, from_email=None, **kwargs): - """Send multipart email + """ + Send multipart email. recipient Email recipient address diff --git a/readthedocs/core/utils/__init__.py b/readthedocs/core/utils/__init__.py index a76f5fcb6ab..e2f4df11a2f 100644 --- a/readthedocs/core/utils/__init__.py +++ b/readthedocs/core/utils/__init__.py @@ -1,4 +1,4 @@ -"""Common utilty functions""" +"""Common utilty functions.""" from __future__ import absolute_import @@ -77,7 +77,8 @@ def cname_to_slug(host): def trigger_build(project, version=None, record=True, force=False, basic=False): - """Trigger build for project and version + """ + Trigger build for project and version. If project has a ``build_queue``, execute task on this build queue. Queue will be prefixed with ``build-`` to unify build queue names. @@ -135,7 +136,8 @@ def trigger_build(project, version=None, record=True, force=False, basic=False): def send_email(recipient, subject, template, template_html, context=None, request=None, from_email=None, **kwargs): # pylint: disable=unused-argument - """Alter context passed in and call email send task + """ + Alter context passed in and call email send task. .. seealso:: @@ -152,7 +154,8 @@ def send_email(recipient, subject, template, template_html, context=None, def slugify(value, *args, **kwargs): - """Add a DNS safe option to slugify + """ + Add a DNS safe option to slugify. :param dns_safe: Remove underscores from slug as well """ @@ -170,9 +173,9 @@ def safe_makedirs(directory_name): """ Safely create a directory. - Makedirs has an issue where it has a race condition around - checking for a directory and then creating it. - This catches the exception in the case where the dir already exists. + Makedirs has an issue where it has a race condition around checking for a + directory and then creating it. This catches the exception in the case where + the dir already exists. """ try: os.makedirs(directory_name) diff --git a/readthedocs/core/utils/extend.py b/readthedocs/core/utils/extend.py index c7f86ee3f2f..918ce0d4e21 100644 --- a/readthedocs/core/utils/extend.py +++ b/readthedocs/core/utils/extend.py @@ -1,4 +1,4 @@ -"""Patterns for extending Read the Docs""" +"""Patterns for extending Read the Docs.""" from __future__ import absolute_import import inspect @@ -9,7 +9,8 @@ def get_override_class(proxy_class, default_class=None): - """Determine which class to use in an override class + """ + Determine which class to use in an override class. The `proxy_class` is the main class that is used, and `default_class` is the default class that this proxy class will instantiate. If `default_class` is @@ -33,7 +34,7 @@ def get_override_class(proxy_class, default_class=None): class SettingsOverrideMeta(type): - """Meta class for passing along classmethod class to the underlying class""" + """Meta class for passing along classmethod class to the underlying class.""" # noqa def __getattr__(cls, attr): # noqa: pep8 false positive proxy_class = get_override_class(cls, getattr(cls, '_default_class')) @@ -42,7 +43,8 @@ def __getattr__(cls, attr): # noqa: pep8 false positive class SettingsOverrideObject(six.with_metaclass(SettingsOverrideMeta, object)): - """Base class for creating class that can be overridden + """ + Base class for creating class that can be overridden. This is used for extension points in the code, where we want to extend a class without monkey patching it. This class will proxy classmethod calls @@ -68,7 +70,8 @@ class without monkey patching it. This class will proxy classmethod calls _override_setting = None def __new__(cls, *args, **kwargs): - """Set up wrapped object + """ + Set up wrapped object. Create an instance of the underlying target class and return instead of this class. diff --git a/readthedocs/core/views/hooks.py b/readthedocs/core/views/hooks.py index 2a438f27da8..a99f66c1169 100644 --- a/readthedocs/core/views/hooks.py +++ b/readthedocs/core/views/hooks.py @@ -107,7 +107,8 @@ def _build_url(url, projects, branches): """ Map a URL onto specific projects to build that are linked to that URL. - Check each of the ``branches`` to see if they are active and should be built. + Check each of the ``branches`` to see if they are active and should be + built. """ ret = "" all_built = {} @@ -152,7 +153,7 @@ def _build_url(url, projects, branches): @csrf_exempt def github_build(request): # noqa: D205 """ - GitHub webhook consumer + GitHub webhook consumer. .. warning:: **DEPRECATED** Use :py:cls:`readthedocs.restapi.views.integrations.GitHubWebhookView` @@ -206,7 +207,8 @@ def github_build(request): # noqa: D205 @csrf_exempt def gitlab_build(request): # noqa: D205 - """GitLab webhook consumer + """ + GitLab webhook consumer. .. warning:: **DEPRECATED** Use :py:cls:`readthedocs.restapi.views.integrations.GitLabWebhookView` @@ -239,7 +241,8 @@ def gitlab_build(request): # noqa: D205 @csrf_exempt def bitbucket_build(request): - """Consume webhooks from multiple versions of Bitbucket's API + """ + Consume webhooks from multiple versions of Bitbucket's API. .. warning:: **DEPRECATED** Use :py:cls:`readthedocs.restapi.views.integrations.BitbucketWebhookView` @@ -307,11 +310,13 @@ def bitbucket_build(request): @csrf_exempt def generic_build(request, project_id_or_slug=None): - """Generic webhook build endpoint + """ + Generic webhook build endpoint. .. warning:: **DEPRECATED** - Use :py:cls:`readthedocs.restapi.views.integrations.GenericWebhookView` - instead of this view function + + Use :py:cls:`readthedocs.restapi.views.integrations.GenericWebhookView` + instead of this view function """ try: project = Project.objects.get(pk=project_id_or_slug) diff --git a/readthedocs/doc_builder/backends/mkdocs.py b/readthedocs/doc_builder/backends/mkdocs.py index 412d34dc22a..54cb593fc17 100644 --- a/readthedocs/doc_builder/backends/mkdocs.py +++ b/readthedocs/doc_builder/backends/mkdocs.py @@ -1,7 +1,7 @@ -"""MkDocs_ backend for building docs. +""" +MkDocs_ backend for building docs. .. _MkDocs: http://www.mkdocs.org/ - """ from __future__ import absolute_import import os @@ -22,10 +22,10 @@ def get_absolute_media_url(): - """Get the fully qualified media URL from settings. + """ + Get the fully qualified media URL from settings. Mkdocs needs a full domain because it tries to link to local media files. - """ media_url = settings.MEDIA_URL @@ -38,7 +38,7 @@ def get_absolute_media_url(): class BaseMkdocs(BaseBuilder): - """Mkdocs builder""" + """Mkdocs builder.""" use_theme = True @@ -50,10 +50,10 @@ def __init__(self, *args, **kwargs): self.root_path = self.version.project.checkout_path(self.version.slug) def load_yaml_config(self): - """Load a YAML config. + """ + Load a YAML config. Raise BuildEnvironmentError if failed due to syntax errors. - """ try: return yaml.safe_load( @@ -74,7 +74,7 @@ def load_yaml_config(self): note,)) def append_conf(self, **__): - """Set mkdocs config values""" + """Set mkdocs config values.""" # Pull mkdocs config data user_config = self.load_yaml_config() diff --git a/readthedocs/doc_builder/environments.py b/readthedocs/doc_builder/environments.py index 2ccc70107c3..f304953e252 100644 --- a/readthedocs/doc_builder/environments.py +++ b/readthedocs/doc_builder/environments.py @@ -1,4 +1,4 @@ -"""Documentation Builder Environments""" +"""Documentation Builder Environments.""" from __future__ import absolute_import from builtins import str @@ -45,7 +45,8 @@ class BuildCommand(BuildCommandResultMixin): - """Wrap command execution for execution in build environments + """ + Wrap command execution for execution in build environments. This wraps subprocess commands with some logic to handle exceptions, logging, and setting up the env for the build command. @@ -101,7 +102,8 @@ def __str__(self): return '\n'.join([self.get_command(), output]) def run(self): - """Set up subprocess and execute command + """ + Set up subprocess and execute command. :param cmd_input: input to pass to command in STDIN :type cmd_input: str @@ -170,13 +172,13 @@ def run(self): self.end_time = datetime.utcnow() def get_command(self): - """Flatten command""" + """Flatten command.""" if hasattr(self.command, '__iter__') and not isinstance(self.command, str): return ' '.join(self.command) return self.command def save(self): - """Save this command and result via the API""" + """Save this command and result via the API.""" data = { 'build': self.build_env.build.get('id'), 'command': self.get_command(), @@ -191,13 +193,15 @@ def save(self): class DockerBuildCommand(BuildCommand): - """Create a docker container and run a command inside the container + """ + Create a docker container and run a command inside the container. Build command to execute in docker container """ def run(self): - """Execute command in existing Docker container + """ + Execute command in existing Docker container. :param cmd_input: input to pass to command in STDIN :type cmd_input: str @@ -241,7 +245,8 @@ def run(self): self.end_time = datetime.utcnow() def get_wrapped_command(self): - """Escape special bash characters in command to wrap in shell + """ + Escape special bash characters in command to wrap in shell. In order to set the current working path inside a docker container, we need to wrap the command in a shell call manually. Some characters will @@ -264,7 +269,8 @@ def get_wrapped_command(self): class BuildEnvironment(object): - """Base build environment + """ + Base build environment. Base class for wrapping command execution for build steps. This provides a context for command execution and reporting, and eventually performs updates @@ -320,7 +326,8 @@ def __exit__(self, exc_type, exc_value, tb): return ret def handle_exception(self, exc_type, exc_value, _): - """Exception handling for __enter__ and __exit__ + """ + Exception handling for __enter__ and __exit__ This reports on the exception we're handling and special cases subclasses of BuildEnvironmentException. For @@ -340,11 +347,12 @@ def handle_exception(self, exc_type, exc_value, _): return True def run(self, *cmd, **kwargs): - """Shortcut to run command from environment""" + """Shortcut to run command from environment.""" return self.run_command_class(cls=self.command_class, cmd=cmd, **kwargs) def run_command_class(self, cls, cmd, **kwargs): - """Run command from this environment + """ + Run command from this environment. Use ``cls`` to instantiate a command @@ -383,13 +391,13 @@ def run_command_class(self, cls, cmd, **kwargs): @property def successful(self): - """Is build completed, without top level failures or failing commands""" + """Is build completed, without top level failures or failing commands.""" # noqa return (self.done and self.failure is None and all(cmd.successful for cmd in self.commands)) @property def failed(self): - """Is build completed, but has top level failure or failing commands""" + """Is build completed, but has top level failure or failing commands.""" return (self.done and ( self.failure is not None or any(cmd.failed for cmd in self.commands) @@ -397,12 +405,13 @@ def failed(self): @property def done(self): - """Is build in finished state""" + """Is build in finished state.""" return (self.build is not None and self.build['state'] == BUILD_STATE_FINISHED) def update_build(self, state=None): - """Record a build by hitting the API + """ + Record a build by hitting the API. This step is skipped if we aren't recording the build. To avoid recording successful builds yet (for instance, running setup commands @@ -488,7 +497,7 @@ def update_build(self, state=None): class LocalEnvironment(BuildEnvironment): - """Local execution environment""" + """Local execution environment.""" command_class = BuildCommand @@ -496,7 +505,7 @@ class LocalEnvironment(BuildEnvironment): class DockerEnvironment(BuildEnvironment): """ - Docker build environment, uses docker to contain builds + Docker build environment, uses docker to contain builds. If :py:data:`settings.DOCKER_ENABLE` is true, build documentation inside a docker container, instead of the host system, using this build environment @@ -532,7 +541,7 @@ def __init__(self, *args, **kwargs): self.container_time_limit = self.project.container_time_limit def __enter__(self): - """Start of environment context""" + """Start of environment context.""" log.info('Creating container') try: # Test for existing container. We remove any stale containers that @@ -579,7 +588,7 @@ def __enter__(self): return self def __exit__(self, exc_type, exc_value, tb): - """End of environment context""" + """End of environment context.""" try: # Update buildenv state given any container error states first self.update_build_from_container_state() @@ -624,7 +633,7 @@ def __exit__(self, exc_type, exc_value, tb): return ret def get_client(self): - """Create Docker client connection""" + """Create Docker client connection.""" try: if self.client is None: self.client = Client( @@ -652,14 +661,14 @@ def get_client(self): @property def container_id(self): - """Return id of container if it is valid""" + """Return id of container if it is valid.""" if self.container_name: return self.container_name elif self.container: return self.container.get('Id') def container_state(self): - """Get container state""" + """Get container state.""" client = self.get_client() try: info = client.inspect_container(self.container_id) @@ -668,7 +677,8 @@ def container_state(self): return None def update_build_from_container_state(self): - """Update buildenv state from container state + """ + Update buildenv state from container state. In the case of the parent command exiting before the exec commands finish and the container is destroyed, or in the case of OOM on the @@ -689,7 +699,7 @@ def update_build_from_container_state(self): .format(state.get('Error')))) def create_container(self): - """Create docker container""" + """Create docker container.""" client = self.get_client() image = self.container_image if self.project.container_image: diff --git a/readthedocs/doc_builder/python_environments.py b/readthedocs/doc_builder/python_environments.py index 1be13186c2d..74879ac4ce7 100644 --- a/readthedocs/doc_builder/python_environments.py +++ b/readthedocs/doc_builder/python_environments.py @@ -76,7 +76,8 @@ def install_package(self): ) def venv_bin(self, filename=None): - """Return path to the virtualenv bin path, or a specific binary + """ + Return path to the virtualenv bin path, or a specific binary. :param filename: If specified, add this filename to the path return :returns: Path to virtualenv bin or filename in virtualenv bin @@ -89,10 +90,10 @@ def venv_bin(self, filename=None): class Virtualenv(PythonEnvironment): - """A virtualenv_ environment. + """ + A virtualenv_ environment. .. _virtualenv: https://virtualenv.pypa.io/ - """ def venv_path(self): @@ -203,10 +204,10 @@ def install_user_requirements(self): class Conda(PythonEnvironment): - """A Conda_ environment. + """ + A Conda_ environment. .. _Conda: https://conda.io/docs/ - """ def venv_path(self): diff --git a/readthedocs/gold/__init__.py b/readthedocs/gold/__init__.py index 1e2be0dc54c..b26c8ed7c84 100644 --- a/readthedocs/gold/__init__.py +++ b/readthedocs/gold/__init__.py @@ -1,6 +1,6 @@ -"""A Django app for Gold Membership. +""" +A Django app for Gold Membership. Gold Membership is Read the Docs' program for recurring, monthly donations. - """ default_app_config = 'readthedocs.gold.apps.GoldAppConfig' diff --git a/readthedocs/gold/forms.py b/readthedocs/gold/forms.py index 3ddfc6aedf5..0e03009d605 100644 --- a/readthedocs/gold/forms.py +++ b/readthedocs/gold/forms.py @@ -1,4 +1,4 @@ -"""Gold subscription forms""" +"""Gold subscription forms.""" from __future__ import absolute_import from builtins import object @@ -12,7 +12,8 @@ class GoldSubscriptionForm(StripeResourceMixin, StripeModelForm): - """Gold subscription payment form + """ + Gold subscription payment form. This extends the common base form for handling Stripe subscriptions. Credit card fields for card number, expiry, and CVV are extended from diff --git a/readthedocs/gold/views.py b/readthedocs/gold/views.py index 08477b1c32e..93fa51fc0dc 100644 --- a/readthedocs/gold/views.py +++ b/readthedocs/gold/views.py @@ -1,4 +1,4 @@ -"""Gold subscription views""" +"""Gold subscription views.""" from __future__ import absolute_import from django.core.urlresolvers import reverse, reverse_lazy @@ -22,7 +22,7 @@ class GoldSubscriptionMixin(SuccessMessageMixin, StripeMixin, LoginRequiredMixin): - """Gold subscription mixin for view classes""" + """Gold subscription mixin for view classes.""" model = GoldUser form_class = GoldSubscriptionForm @@ -34,7 +34,7 @@ def get_object(self): return None def get_form(self, data=None, files=None, **kwargs): - """Pass in copy of POST data to avoid read only QueryDicts""" + """Pass in copy of POST data to avoid read only QueryDicts.""" kwargs['customer'] = self.request.user return super(GoldSubscriptionMixin, self).get_form(data, files, **kwargs) @@ -57,7 +57,8 @@ def get_context_data(self, **kwargs): class DetailGoldSubscription(GoldSubscriptionMixin, DetailView): def get(self, request, *args, **kwargs): - """GET handling for this view + """ + GET handling for this view. If there is a gold subscription instance, then we show the normal detail page, otherwise show the registration form @@ -74,7 +75,8 @@ class UpdateGoldSubscription(GoldSubscriptionMixin, UpdateView): class DeleteGoldSubscription(GoldSubscriptionMixin, DeleteView): - """Delete Gold subscription view + """ + Delete Gold subscription view. On object deletion, the corresponding Stripe customer is deleted as well. Deletion is triggered on subscription deletion using a signal, ensuring the @@ -84,7 +86,7 @@ class DeleteGoldSubscription(GoldSubscriptionMixin, DeleteView): success_message = _('Your subscription has been cancelled') def post(self, request, *args, **kwargs): - """Add success message to delete post""" + """Add success message to delete post.""" resp = super(DeleteGoldSubscription, self).post(request, *args, **kwargs) success_message = self.get_success_message({}) if success_message: diff --git a/readthedocs/integrations/admin.py b/readthedocs/integrations/admin.py index 5cf5264f80f..00cb1390826 100644 --- a/readthedocs/integrations/admin.py +++ b/readthedocs/integrations/admin.py @@ -1,4 +1,4 @@ -"""Integration admin models""" +"""Integration admin models.""" from __future__ import absolute_import from django.contrib import admin @@ -30,7 +30,8 @@ def inner(_, obj): class HttpExchangeAdmin(admin.ModelAdmin): - """Admin model for HttpExchange + """ + Admin model for HttpExchange. This adds some read-only display to the admin model. """ @@ -78,7 +79,8 @@ def failed_icon(self, obj): class IntegrationAdmin(admin.ModelAdmin): - """Admin model for Integration + """ + Admin model for Integration. Because of some problems using JSONField with admin model inlines, this instead just links to the queryset. @@ -88,7 +90,8 @@ class IntegrationAdmin(admin.ModelAdmin): readonly_fields = ['exchanges'] def exchanges(self, obj): - """Manually make an inline-ish block + """ + Manually make an inline-ish block. JSONField doesn't do well with fieldsets for whatever reason. This is just to link to the exchanges. diff --git a/readthedocs/integrations/utils.py b/readthedocs/integrations/utils.py index 338d86805ed..978da9c8504 100644 --- a/readthedocs/integrations/utils.py +++ b/readthedocs/integrations/utils.py @@ -1,8 +1,9 @@ -"""Integration utility functions""" +"""Integration utility functions.""" def normalize_request_payload(request): - """Normalize the request body, hopefully to JSON + """ + Normalize the request body, hopefully to JSON. This will attempt to return a JSON body, backing down to a string body next. diff --git a/readthedocs/notifications/__init__.py b/readthedocs/notifications/__init__.py index 1fbd93edeaa..518d5db8410 100644 --- a/readthedocs/notifications/__init__.py +++ b/readthedocs/notifications/__init__.py @@ -1,4 +1,5 @@ -"""Extensions to Django messages to support notifications to users. +""" +Extensions to Django messages to support notifications to users. Notifications are important communications to users that need to be as visible as possible. We support different backends to make notifications visible in @@ -10,7 +11,6 @@ .. _`django-messages-extends`: https://github.com /AliLozano/django-messages-extends/ - """ from .notification import Notification from .backends import send_notification diff --git a/readthedocs/notifications/backends.py b/readthedocs/notifications/backends.py index c82dc2243c1..48bb0dd0556 100644 --- a/readthedocs/notifications/backends.py +++ b/readthedocs/notifications/backends.py @@ -1,9 +1,9 @@ -"""Pluggable backends for the delivery of notifications. +""" +Pluggable backends for the delivery of notifications. Delivery of notifications to users depends on a list of backends configured in Django settings. For example, they might be e-mailed to users as well as displayed on the site. - """ from __future__ import absolute_import @@ -19,7 +19,8 @@ def send_notification(request, notification): - """Send notifications through all backends defined by settings + """ + Send notifications through all backends defined by settings. Backends should be listed in the settings ``NOTIFICATION_BACKENDS``, which should be a list of class paths to be loaded, using the standard Django @@ -42,7 +43,8 @@ def send(self, notification): class EmailBackend(Backend): - """Send templated notification emails through our standard email backend + """ + Send templated notification emails through our standard email backend. The content body is first rendered from an on-disk template, then passed into the standard email templates as a string. @@ -66,7 +68,8 @@ def send(self, notification): class SiteBackend(Backend): - """Add messages through Django messages application + """ + Add messages through Django messages application. This uses persistent messageing levels provided by :py:mod:`message_extends` and stores persistent messages in the database. diff --git a/readthedocs/notifications/forms.py b/readthedocs/notifications/forms.py index d75427a5be7..b65c1c15e76 100644 --- a/readthedocs/notifications/forms.py +++ b/readthedocs/notifications/forms.py @@ -6,7 +6,8 @@ class SendNotificationForm(forms.Form): - """Send notification form + """ + Send notification form. Used for sending a notification to a list of users from admin pages @@ -33,7 +34,7 @@ def __init__(self, *args, **kwargs): in self.notification_classes] def clean_source(self): - """Get the source class from the class name""" + """Get the source class from the class name.""" source = self.cleaned_data['source'] classes = dict((cls.name, cls) for cls in self.notification_classes) return classes.get(source, None) diff --git a/readthedocs/notifications/notification.py b/readthedocs/notifications/notification.py index b5da15a982f..261b0965b30 100644 --- a/readthedocs/notifications/notification.py +++ b/readthedocs/notifications/notification.py @@ -13,14 +13,14 @@ class Notification(object): - """An unsent notification linked to an object. + """ + An unsent notification linked to an object. This class provides an interface to construct notification messages by rendering Django templates. The ``Notification`` itself is not expected to be persisted by the backends. Call .send() to send the notification. - """ name = None @@ -75,7 +75,8 @@ def render(self, backend_name, source_format=constants.HTML): ) def send(self): - """Trigger notification send through all notification backends + """ + Trigger notification send through all notification backends. In order to limit which backends a notification will send out from, override this method and duplicate the logic from diff --git a/readthedocs/notifications/storages.py b/readthedocs/notifications/storages.py index e67139b709c..2c674b7211d 100644 --- a/readthedocs/notifications/storages.py +++ b/readthedocs/notifications/storages.py @@ -10,7 +10,8 @@ class FallbackUniqueStorage(FallbackStorage): - """Persistent message fallback storage, but only stores unique notifications + """ + Persistent message fallback storage, but only stores unique notifications. This loops through all backends to find messages to store, but will skip this step if the message already exists for the user in the database. diff --git a/readthedocs/notifications/views.py b/readthedocs/notifications/views.py index e3f3cbe8d05..df1a020c369 100644 --- a/readthedocs/notifications/views.py +++ b/readthedocs/notifications/views.py @@ -9,7 +9,8 @@ class SendNotificationView(FormView): - """Form view for sending notifications to users from admin pages + """ + Form view for sending notifications to users from admin pages. Accepts the following additional parameters: @@ -28,7 +29,8 @@ class SendNotificationView(FormView): notification_classes = [] def get_form_kwargs(self): - """Override form kwargs based on input fields + """ + Override form kwargs based on input fields. The admin posts to this view initially, so detect the send button on form post variables. Drop additional fields if we see the send button. @@ -41,14 +43,14 @@ def get_form_kwargs(self): return kwargs def get_initial(self): - """Add selected ids to initial form data""" + """Add selected ids to initial form data.""" initial = super(SendNotificationView, self).get_initial() initial['_selected_action'] = self.request.POST.getlist( admin.ACTION_CHECKBOX_NAME) return initial def form_valid(self, form): - """If form is valid, send notification to recipients""" + """If form is valid, send notification to recipients.""" count = 0 notification_cls = form.cleaned_data['source'] for obj in self.get_queryset().all(): @@ -65,7 +67,8 @@ def form_valid(self, form): return HttpResponseRedirect(self.request.get_full_path()) def get_object_recipients(self, obj): - """Iterate over queryset objects and return User objects + """ + Iterate over queryset objects and return User objects. This allows for non-User querysets to pass back a list of Users to send to. By default, assume we're working with :py:class:`User` objects and @@ -85,7 +88,7 @@ def get_queryset(self): return self.kwargs.get('queryset') def get_context_data(self, **kwargs): - """Return queryset in context""" + """Return queryset in context.""" context = super(SendNotificationView, self).get_context_data(**kwargs) recipients = [] for obj in self.get_queryset().all(): @@ -96,7 +99,8 @@ def get_context_data(self, **kwargs): def message_user(self, message, level=messages.INFO, extra_tags='', fail_silently=False): - """Implementation of :py:meth:`django.contrib.admin.options.ModelAdmin.message_user` + """ + Implementation of :py:meth:`django.contrib.admin.options.ModelAdmin.message_user` Send message through messages framework """ diff --git a/readthedocs/oauth/services/bitbucket.py b/readthedocs/oauth/services/bitbucket.py index 6c9d8f0b002..eb92e7934c0 100644 --- a/readthedocs/oauth/services/bitbucket.py +++ b/readthedocs/oauth/services/bitbucket.py @@ -1,4 +1,4 @@ -"""OAuth utility functions""" +"""OAuth utility functions.""" from __future__ import absolute_import from builtins import str @@ -26,7 +26,7 @@ class BitbucketService(Service): - """Provider service for Bitbucket""" + """Provider service for Bitbucket.""" adapter = BitbucketOAuth2Adapter # TODO replace this with a less naive check @@ -34,12 +34,12 @@ class BitbucketService(Service): https_url_pattern = re.compile(r'^https:\/\/[^@]+@bitbucket.org/') def sync(self): - """Sync repositories and teams from Bitbucket API""" + """Sync repositories and teams from Bitbucket API.""" self.sync_repositories() self.sync_teams() def sync_repositories(self): - """Sync repositories from Bitbucket API""" + """Sync repositories from Bitbucket API.""" # Get user repos try: repos = self.paginate( @@ -71,7 +71,7 @@ def sync_repositories(self): pass def sync_teams(self): - """Sync Bitbucket teams and team repositories""" + """Sync Bitbucket teams and team repositories.""" try: teams = self.paginate( 'https://api.bitbucket.org/2.0/teams/?role=member' @@ -89,7 +89,8 @@ def sync_teams(self): def create_repository(self, fields, privacy=DEFAULT_PRIVACY_LEVEL, organization=None): - """Update or create a repository from Bitbucket API response + """ + Update or create a repository from Bitbucket API response. .. note:: The :py:data:`admin` property is not set during creation, as @@ -145,7 +146,8 @@ def create_repository(self, fields, privacy=DEFAULT_PRIVACY_LEVEL, fields['name']) def create_organization(self, fields): - """Update or create remote organization from Bitbucket API response + """ + Update or create remote organization from Bitbucket API response. :param fields: dictionary response of data from API :rtype: RemoteOrganization @@ -171,7 +173,7 @@ def get_paginated_results(self, response): return response.json().get('values', []) def get_webhook_data(self, project, integration): - """Get webhook JSON data to post to the API""" + """Get webhook JSON data to post to the API.""" return json.dumps({ 'description': 'Read the Docs ({domain})'.format(domain=settings.PRODUCTION_DOMAIN), 'url': 'https://{domain}{path}'.format( @@ -187,7 +189,8 @@ def get_webhook_data(self, project, integration): }) def setup_webhook(self, project): - """Set up Bitbucket project webhook for project + """ + Set up Bitbucket project webhook for project. :param project: project to set up webhook for :type project: Project @@ -231,7 +234,8 @@ def setup_webhook(self, project): return (False, resp) def update_webhook(self, project, integration): - """Update webhook integration + """ + Update webhook integration. :param project: project to set up webhook for :type project: Project diff --git a/readthedocs/oauth/services/github.py b/readthedocs/oauth/services/github.py index e688c554b21..cf6902ad26d 100644 --- a/readthedocs/oauth/services/github.py +++ b/readthedocs/oauth/services/github.py @@ -1,4 +1,4 @@ -"""OAuth utility functions""" +"""OAuth utility functions.""" from __future__ import absolute_import from builtins import str @@ -27,19 +27,19 @@ class GitHubService(Service): - """Provider service for GitHub""" + """Provider service for GitHub.""" adapter = GitHubOAuth2Adapter # TODO replace this with a less naive check url_pattern = re.compile(r'github\.com') def sync(self): - """Sync repositories and organizations""" + """Sync repositories and organizations.""" self.sync_repositories() self.sync_organizations() def sync_repositories(self): - """Sync repositories from GitHub API""" + """Sync repositories from GitHub API.""" repos = self.paginate('https://api.github.com/user/repos?per_page=100') try: for repo in repos: @@ -51,7 +51,7 @@ def sync_repositories(self): 'try reconnecting your account') def sync_organizations(self): - """Sync organizations from GitHub API""" + """Sync organizations from GitHub API.""" try: orgs = self.paginate('https://api.github.com/user/orgs') for org in orgs: @@ -72,7 +72,8 @@ def sync_organizations(self): def create_repository(self, fields, privacy=DEFAULT_PRIVACY_LEVEL, organization=None): - """Update or create a repository from GitHub API response + """ + Update or create a repository from GitHub API response. :param fields: dictionary of response data from API :param privacy: privacy level to support @@ -122,7 +123,8 @@ def create_repository(self, fields, privacy=DEFAULT_PRIVACY_LEVEL, fields['name']) def create_organization(self, fields): - """Update or create remote organization from GitHub API response + """ + Update or create remote organization from GitHub API response. :param fields: dictionary response of data from API :rtype: RemoteOrganization @@ -155,7 +157,7 @@ def get_paginated_results(self, response): return response.json() def get_webhook_data(self, project, integration): - """Get webhook JSON data to post to the API""" + """Get webhook JSON data to post to the API.""" return json.dumps({ 'name': 'web', 'active': True, @@ -174,7 +176,8 @@ def get_webhook_data(self, project, integration): }) def setup_webhook(self, project): - """Set up GitHub project webhook for project + """ + Set up GitHub project webhook for project. :param project: project to set up webhook for :type project: Project @@ -221,7 +224,8 @@ def setup_webhook(self, project): return (False, resp) def update_webhook(self, project, integration): - """Update webhook integration + """ + Update webhook integration. :param project: project to set up webhook for :type project: Project @@ -265,7 +269,7 @@ def update_webhook(self, project, integration): @classmethod def get_token_for_project(cls, project, force_local=False): - """Get access token for project by iterating over project users""" + """Get access token for project by iterating over project users.""" # TODO why does this only target GitHub? if not getattr(settings, 'ALLOW_PRIVATE_REPOS', False): return None diff --git a/readthedocs/payments/forms.py b/readthedocs/payments/forms.py index 154e5d9151d..0d655ef1584 100644 --- a/readthedocs/payments/forms.py +++ b/readthedocs/payments/forms.py @@ -1,4 +1,4 @@ -"""Payment forms""" +"""Payment forms.""" from __future__ import absolute_import from builtins import str @@ -17,7 +17,7 @@ class StripeResourceMixin(object): - """Stripe actions for resources, available as a Form mixin class""" + """Stripe actions for resources, available as a Form mixin class.""" def ensure_stripe_resource(self, resource, attrs): try: @@ -59,7 +59,8 @@ def get_charge(self): class StripeModelForm(forms.ModelForm): - """Payment form base for Stripe interaction + """ + Payment form base for Stripe interaction. Use this as a base class for payment forms. It includes the necessary fields for card input and manipulates the Knockout field data bindings correctly. @@ -114,7 +115,8 @@ def __init__(self, *args, **kwargs): super(StripeModelForm, self).__init__(*args, **kwargs) def validate_stripe(self): - """Run validation against Stripe + """ + Run validation against Stripe. This is what will create several objects using the Stripe API. We need to actually create the objects, as that is what will provide us with @@ -133,12 +135,12 @@ def clean_stripe_token(self): return data def clean(self): - """Clean form to add Stripe objects via API during validation phase + """ + Clean form to add Stripe objects via API during validation phase. This will handle ensuring a customer and subscription exist and will - raise any issues as validation errors. This is required because part - of Stripe's validation happens on the API call to establish a - subscription. + raise any issues as validation errors. This is required because part of + Stripe's validation happens on the API call to establish a subscription. """ cleaned_data = super(StripeModelForm, self).clean() @@ -171,7 +173,8 @@ def clean(self): return cleaned_data def clear_card_data(self): - """Clear card data on validation errors + """ + Clear card data on validation errors. This requires the form was created by passing in a mutable QueryDict instance, see :py:class:`readthedocs.payments.mixin.StripeMixin` diff --git a/readthedocs/payments/mixins.py b/readthedocs/payments/mixins.py index a8101e462a7..0219da08098 100644 --- a/readthedocs/payments/mixins.py +++ b/readthedocs/payments/mixins.py @@ -1,4 +1,4 @@ -"""Payment view mixin classes""" +"""Payment view mixin classes.""" from __future__ import absolute_import from builtins import object @@ -7,7 +7,7 @@ class StripeMixin(object): - """Adds Stripe publishable key to the context data""" + """Adds Stripe publishable key to the context data.""" def get_context_data(self, **kwargs): context = super(StripeMixin, self).get_context_data(**kwargs) @@ -15,7 +15,8 @@ def get_context_data(self, **kwargs): return context def get_form(self, data=None, files=None, **kwargs): - """Pass in copy of POST data to avoid read only QueryDicts on form + """ + Pass in copy of POST data to avoid read only QueryDicts on form. This is used to be able to reset some important credit card fields if card validation fails. In this case, the Stripe token was valid, but the diff --git a/readthedocs/payments/utils.py b/readthedocs/payments/utils.py index cf3722371d3..a65b5b0a8f6 100644 --- a/readthedocs/payments/utils.py +++ b/readthedocs/payments/utils.py @@ -1,4 +1,5 @@ -"""Payment utility functions +""" +Payment utility functions. These are mostly one-off functions. Define the bulk of Stripe operations on :py:class:`readthedocs.payments.forms.StripeResourceMixin`. @@ -12,7 +13,7 @@ def delete_customer(customer_id): - """Delete customer from Stripe, cancelling subscriptions""" + """Delete customer from Stripe, cancelling subscriptions.""" try: customer = stripe.Customer.retrieve(customer_id) return customer.delete() @@ -21,7 +22,7 @@ def delete_customer(customer_id): def cancel_subscription(customer_id, subscription_id): - """Cancel Stripe subscription, if it exists""" + """Cancel Stripe subscription, if it exists.""" try: customer = stripe.Customer.retrieve(customer_id) if hasattr(customer, 'subscriptions'): diff --git a/readthedocs/projects/admin.py b/readthedocs/projects/admin.py index faa3690e04d..cca3e9c3ce4 100644 --- a/readthedocs/projects/admin.py +++ b/readthedocs/projects/admin.py @@ -74,7 +74,8 @@ class DomainInline(admin.TabularInline): class ProjectOwnerBannedFilter(admin.SimpleListFilter): - """Filter for projects with banned owners + """ + Filter for projects with banned owners. There are problems adding `users__profile__banned` to the `list_filter` attribute, so we'll create a basic filter to capture banned owners. @@ -98,7 +99,7 @@ def queryset(self, request, queryset): class ProjectAdmin(GuardedModelAdmin): - """Project model admin view""" + """Project model admin view.""" prepopulated_fields = {'slug': ('name',)} list_display = ('name', 'repo', 'repo_type', 'allow_comments', 'featured', 'theme') @@ -121,7 +122,8 @@ def send_owner_email(self, request, queryset): send_owner_email.short_description = 'Notify project owners' def ban_owner(self, request, queryset): - """Ban project owner + """ + Ban project owner. This will only ban single owners, because a malicious user could add a user as a co-owner of the project. We don't want to induce and @@ -146,7 +148,8 @@ def ban_owner(self, request, queryset): ban_owner.short_description = 'Ban project owner' def delete_selected_and_artifacts(self, request, queryset): - """Remove HTML/etc artifacts from application instances + """ + Remove HTML/etc artifacts from application instances. Prior to the query delete, broadcast tasks to delete HTML artifacts from application instances. diff --git a/readthedocs/projects/exceptions.py b/readthedocs/projects/exceptions.py index e8f8d0fad8d..5209472aa3a 100644 --- a/readthedocs/projects/exceptions.py +++ b/readthedocs/projects/exceptions.py @@ -1,4 +1,4 @@ -"""Project exceptions""" +"""Project exceptions.""" from django.conf import settings from django.utils.translation import ugettext_noop as _ @@ -8,7 +8,7 @@ class ProjectConfigurationError(BuildEnvironmentError): - """Error raised trying to configure a project for build""" + """Error raised trying to configure a project for build.""" NOT_FOUND = _( 'A configuration file was not found. ' @@ -38,7 +38,8 @@ def get_default_message(self): class ProjectSpamError(Exception): - """Error raised when a project field has detected spam + """ + Error raised when a project field has detected spam. This error is not raised to users, we use this for banning users in the background. diff --git a/readthedocs/projects/models.py b/readthedocs/projects/models.py index 12dbb020ebc..112e458c194 100644 --- a/readthedocs/projects/models.py +++ b/readthedocs/projects/models.py @@ -1,4 +1,4 @@ -"""Project models""" +"""Project models.""" from __future__ import absolute_import @@ -43,7 +43,8 @@ @python_2_unicode_compatible class ProjectRelationship(models.Model): - """Project to project relationship + """ + Project to project relationship. This is used for subprojects """ @@ -72,7 +73,7 @@ def get_absolute_url(self): @python_2_unicode_compatible class Project(models.Model): - """Project model""" + """Project model.""" # Auto fields pub_date = models.DateTimeField(_('Publication date'), auto_now_add=True) @@ -343,7 +344,8 @@ def get_absolute_url(self): return reverse('projects_detail', args=[self.slug]) def get_docs_url(self, version_slug=None, lang_slug=None, private=None): - """Return a URL for the docs + """ + Return a URL for the docs. Always use http for now, to avoid content warnings. """ @@ -360,7 +362,8 @@ def get_canonical_url(self): return self.get_docs_url() def get_subproject_urls(self): - """List subproject URLs + """ + List subproject URLs. This is used in search result linking """ @@ -375,12 +378,13 @@ def get_subproject_urls(self): def get_production_media_path(self, type_, version_slug, include_file=True): """ - This is used to see if these files exist so we can offer them for download. + Used to see if these files exist so we can offer them for download. :param type_: Media content type, ie - 'pdf', 'zip' :param version_slug: Project version slug for lookup :param include_file: Include file name in return :type include_file: bool + :returns: Full path to media file or path """ if getattr(settings, 'DEFAULT_PRIVACY_LEVEL', 'public') == 'public' or settings.DEBUG: @@ -409,7 +413,7 @@ def get_production_media_url(self, type_, version_slug, full_path=True): return path def subdomain(self): - """Get project subdomain from resolver""" + """Get project subdomain from resolver.""" return resolve_domain(self) def get_downloads(self): @@ -440,7 +444,7 @@ def checkout_path(self, version=LATEST): @property def pip_cache_path(self): - """Path to pip cache""" + """Path to pip cache.""" if getattr(settings, 'GLOBAL_PIP_CACHE', False): return settings.GLOBAL_PIP_CACHE return os.path.join(self.doc_path, '.cache', 'pip') @@ -449,7 +453,7 @@ def pip_cache_path(self): # Paths for symlinks in project doc_path. # def translations_symlink_path(self, language=None): - """Path in the doc_path that we symlink translations""" + """Path in the doc_path that we symlink translations.""" if not language: language = self.language return os.path.join(self.doc_path, 'translations', language) @@ -459,7 +463,7 @@ def translations_symlink_path(self, language=None): # def full_doc_path(self, version=LATEST): - """The path to the documentation root in the project""" + """The path to the documentation root in the project.""" doc_base = self.checkout_path(version) for possible_path in ['docs', 'doc', 'Doc']: if os.path.exists(os.path.join(doc_base, '%s' % possible_path)): @@ -468,19 +472,19 @@ def full_doc_path(self, version=LATEST): return doc_base def artifact_path(self, type_, version=LATEST): - """The path to the build html docs in the project""" + """The path to the build html docs in the project.""" return os.path.join(self.doc_path, "artifacts", version, type_) def full_build_path(self, version=LATEST): - """The path to the build html docs in the project""" + """The path to the build html docs in the project.""" return os.path.join(self.conf_dir(version), "_build", "html") def full_latex_path(self, version=LATEST): - """The path to the build LaTeX docs in the project""" + """The path to the build LaTeX docs in the project.""" return os.path.join(self.conf_dir(version), "_build", "latex") def full_epub_path(self, version=LATEST): - """The path to the build epub docs in the project""" + """The path to the build epub docs in the project.""" return os.path.join(self.conf_dir(version), "_build", "epub") # There is currently no support for building man/dash formats, but we keep @@ -488,34 +492,34 @@ def full_epub_path(self, version=LATEST): # legacy builds. def full_man_path(self, version=LATEST): - """The path to the build man docs in the project""" + """The path to the build man docs in the project.""" return os.path.join(self.conf_dir(version), "_build", "man") def full_dash_path(self, version=LATEST): - """The path to the build dash docs in the project""" + """The path to the build dash docs in the project.""" return os.path.join(self.conf_dir(version), "_build", "dash") def full_json_path(self, version=LATEST): - """The path to the build json docs in the project""" + """The path to the build json docs in the project.""" if 'sphinx' in self.documentation_type: return os.path.join(self.conf_dir(version), "_build", "json") elif 'mkdocs' in self.documentation_type: return os.path.join(self.checkout_path(version), "_build", "json") def full_singlehtml_path(self, version=LATEST): - """The path to the build singlehtml docs in the project""" + """The path to the build singlehtml docs in the project.""" return os.path.join(self.conf_dir(version), "_build", "singlehtml") def rtd_build_path(self, version=LATEST): - """The destination path where the built docs are copied""" + """The destination path where the built docs are copied.""" return os.path.join(self.doc_path, 'rtd-builds', version) def static_metadata_path(self): - """The path to the static metadata JSON settings file""" + """The path to the static metadata JSON settings file.""" return os.path.join(self.doc_path, 'metadata.json') def conf_file(self, version=LATEST): - """Find a ``conf.py`` file in the project checkout""" + """Find a ``conf.py`` file in the project checkout.""" if self.conf_py_file: conf_path = os.path.join(self.checkout_path(version), self.conf_py_file) if os.path.exists(conf_path): @@ -542,12 +546,12 @@ def conf_dir(self, version=LATEST): @property def is_type_sphinx(self): - """Is project type Sphinx""" + """Is project type Sphinx.""" return 'sphinx' in self.documentation_type @property def is_type_mkdocs(self): - """Is project type Mkdocs""" + """Is project type Mkdocs.""" return 'mkdocs' in self.documentation_type @property @@ -603,7 +607,8 @@ def repo_lock(self, version, timeout=5, polling_interval=5): return Lock(self, version, timeout, polling_interval) def find(self, filename, version): - """Find files inside the project's ``doc`` path + """ + Find files inside the project's ``doc`` path. :param filename: Filename to search for in project checkout :param version: Version instance to set version checkout path @@ -615,7 +620,8 @@ def find(self, filename, version): return matches def full_find(self, filename, version): - """Find files inside a project's checkout path + """ + Find files inside a project's checkout path. :param filename: Filename to search for in project checkout :param version: Version instance to set version checkout path @@ -628,10 +634,9 @@ def full_find(self, filename, version): def get_latest_build(self, finished=True): """ - Get latest build for project + Get latest build for project. - finished - Return only builds that are in a finished state + finished -- Return only builds that are in a finished state """ kwargs = {'type': 'html'} if finished: @@ -664,7 +669,8 @@ def ordered_active_versions(self, user=None): return sort_version_aware(versions) def all_active_versions(self): - """Get queryset with all active versions + """ + Get queryset with all active versions. .. note:: This is a temporary workaround for activate_versions filtering out @@ -675,7 +681,8 @@ def all_active_versions(self): return self.versions.filter(active=True) def supported_versions(self): - """Get the list of supported versions + """ + Get the list of supported versions. :returns: List of version strings. """ @@ -693,7 +700,8 @@ def get_stable_version(self): return self.versions.filter(slug=STABLE).first() def update_stable_version(self): - """Returns the version that was promoted to be the new stable version + """ + Returns the version that was promoted to be the new stable version. Return ``None`` if no update was mode or if there is no version on the project that can be considered stable. @@ -749,7 +757,7 @@ def get_default_version(self): return LATEST def get_default_branch(self): - """Get the version representing 'latest'""" + """Get the version representing 'latest'.""" if self.default_branch: return self.default_branch return self.vcs_repo().fallback_branch @@ -776,7 +784,8 @@ def moderation_queue(self): return queue def add_node(self, content_hash, page, version, commit): - """Add comment node + """ + Add comment node. :param content_hash: Hash of node content :param page: Doc page for node @@ -804,7 +813,8 @@ def add_node(self, content_hash, page, version, commit): return True # ie, it's True that a new node was created. def add_comment(self, version_slug, page, content_hash, commit, user, text): - """Add comment to node + """ + Add comment to node. :param version_slug: Version slug to use for node lookup :param page: Page to attach comment to @@ -827,7 +837,8 @@ def features(self): return Feature.objects.for_project(self) def has_feature(self, feature_id): - """Does project have existing feature flag + """ + Does project have existing feature flag. If the feature has a historical True value before the feature was added, we consider the project to have the flag. This is used for deprecating a @@ -836,7 +847,8 @@ def has_feature(self, feature_id): return self.features.filter(feature_id=feature_id).exists() def get_feature_value(self, feature, positive, negative): - """Look up project feature, return corresponding value + """ + Look up project feature, return corresponding value. If a project has a feature, return ``positive``, otherwise return ``negative`` @@ -846,7 +858,8 @@ def get_feature_value(self, feature, positive, negative): class APIProject(Project): - """Project proxy model for API data deserialization + """ + Project proxy model for API data deserialization. This replaces the pattern where API data was deserialized into a mocked :py:cls:`Project` object. This pattern was confusing, as it was not explicit @@ -885,7 +898,8 @@ def has_feature(self, feature_id): @python_2_unicode_compatible class ImportedFile(models.Model): - """Imported files model + """ + Imported files model. This tracks files that are output from documentation builds, useful for things like CDN invalidation. @@ -986,7 +1000,8 @@ def delete(self, *args, **kwargs): # pylint: disable=arguments-differ @python_2_unicode_compatible class Feature(models.Model): - """Project feature flags + """ + Project feature flags. Features should generally be added here as choices, however features may also be added dynamically from a signal in other packages. Features can be @@ -1041,7 +1056,8 @@ def __str__(self): ) def get_feature_display(self): - """Implement display name field for fake ChoiceField + """ + Implement display name field for fake ChoiceField. Because the field is not a ChoiceField here, we need to manually implement this behavior. diff --git a/readthedocs/projects/search_indexes.py b/readthedocs/projects/search_indexes.py index 84e2ba2bc6d..5300fea5b9a 100644 --- a/readthedocs/projects/search_indexes.py +++ b/readthedocs/projects/search_indexes.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- """ -Project search indexes +Project search indexes. .. deprecated:: - Read the Docs no longer uses Haystack in production and the core team does - not maintain this code. Use at your own risk, this may go away soon. + + Read the Docs no longer uses Haystack in production and the core team does not + maintain this code. Use at your own risk, this may go away soon. """ from __future__ import absolute_import @@ -26,7 +27,7 @@ class ProjectIndex(indexes.SearchIndex, indexes.Indexable): - """Project search index""" + """Project search index.""" text = CharField(document=True, use_template=True) author = CharField() @@ -45,14 +46,14 @@ def get_model(self): return Project def index_queryset(self, using=None): - """Used when the entire index for model is updated""" + """Used when the entire index for model is updated.""" return self.get_model().objects.public() # TODO Should prob make a common subclass for this and FileIndex class ImportedFileIndex(indexes.SearchIndex, indexes.Indexable): - """Search index for imported files""" + """Search index for imported files.""" text = CharField(document=True) author = CharField() @@ -71,7 +72,8 @@ def prepare_absolute_url(self, obj): return obj.get_absolute_url() def prepare_text(self, obj): - """Prepare the text of the html file + """ + Prepare the text of the html file. This only works on machines that have the html files for the projects checked out. @@ -107,6 +109,6 @@ def get_model(self): return ImportedFile def index_queryset(self, using=None): - """Used when the entire index for model is updated""" + """Used when the entire index for model is updated.""" return (self.get_model().objects .filter(project__privacy_level=constants.PUBLIC)) diff --git a/readthedocs/projects/tasks.py b/readthedocs/projects/tasks.py index b0a39725f69..0bf82c85500 100644 --- a/readthedocs/projects/tasks.py +++ b/readthedocs/projects/tasks.py @@ -1,4 +1,5 @@ -"""Tasks related to projects +""" +Tasks related to projects. This includes fetching repository code, cleaning ``conf.py`` files, and rebuilding documentation. @@ -186,7 +187,8 @@ def run(self, pk, version_pk=None, build_pk=None, record=True, return True def run_setup(self, record=True): - """Run setup in the local environment. + """ + Run setup in the local environment. Return True if successful. """ @@ -236,11 +238,11 @@ def run_setup(self, record=True): return True def run_build(self, docker=False, record=True): - """Build the docs in an environment. + """ + Build the docs in an environment. If `docker` is True, or Docker is enabled by the settings.DOCKER_ENABLE setting, then build in a Docker environment. Otherwise build locally. - """ env_vars = self.get_env_vars() @@ -294,13 +296,13 @@ def run_build(self, docker=False, record=True): @staticmethod def get_project(project_pk): - """Get project from API""" + """Get project from API.""" project_data = api_v2.project(project_pk).get() return APIProject(**project_data) @staticmethod def get_version(project, version_pk): - """Ensure we're using a sane version""" + """Ensure we're using a sane version.""" if version_pk: version_data = api_v2.version(version_pk).get() else: @@ -312,7 +314,7 @@ def get_version(project, version_pk): @staticmethod def get_build(build_pk): """ - Retrieve build object from API + Retrieve build object from API. :param build_pk: Build primary key """ @@ -369,7 +371,7 @@ def set_valid_clone(self): def update_documentation_type(self): """ - Force Sphinx for 'auto' documentation type + Force Sphinx for 'auto' documentation type. This used to determine the type and automatically set the documentation type to Sphinx for rST and Mkdocs for markdown. It now just forces @@ -383,7 +385,8 @@ def update_documentation_type(self): def update_app_instances(self, html=False, localmedia=False, search=False, pdf=False, epub=False): - """Update application instances with build artifacts + """ + Update application instances with build artifacts. This triggers updates across application instances for html, pdf, epub, downloads, and search. Tasks are broadcast to all web servers from here. @@ -439,7 +442,8 @@ def setup_environment(self): self.python_env.install_package() def build_docs(self): - """Wrapper to all build functions + """ + Wrapper to all build functions. Executes the necessary builds for this task and returns whether the build was successful or not. @@ -465,7 +469,7 @@ def build_docs(self): return outcomes def build_docs_html(self): - """Build HTML docs""" + """Build HTML docs.""" html_builder = get_builder_class(self.project.documentation_type)( build_env=self.build_env, python_env=self.python_env, @@ -489,7 +493,7 @@ def build_docs_html(self): return success def build_docs_search(self): - """Build search data with separate build""" + """Build search data with separate build.""" if self.build_search: if self.project.is_type_mkdocs: return self.build_docs_class('mkdocs_json') @@ -498,7 +502,7 @@ def build_docs_search(self): return False def build_docs_localmedia(self): - """Get local media files with separate build""" + """Get local media files with separate build.""" if 'htmlzip' not in self.config.formats: return False @@ -508,7 +512,7 @@ def build_docs_localmedia(self): return False def build_docs_pdf(self): - """Build PDF docs""" + """Build PDF docs.""" if ('pdf' not in self.config.formats or self.project.slug in HTML_ONLY or not self.project.is_type_sphinx): @@ -516,7 +520,7 @@ def build_docs_pdf(self): return self.build_docs_class('sphinx_pdf') def build_docs_epub(self): - """Build ePub docs""" + """Build ePub docs.""" if ('epub' not in self.config.formats or self.project.slug in HTML_ONLY or not self.project.is_type_sphinx): @@ -524,7 +528,8 @@ def build_docs_epub(self): return self.build_docs_class('sphinx_epub') def build_docs_class(self, builder_class): - """Build docs with additional doc backends + """ + Build docs with additional doc backends. These steps are not necessarily required for the build to halt, so we only raise a warning exception here. A hard error will halt the build @@ -536,14 +541,14 @@ def build_docs_class(self, builder_class): return success def send_notifications(self): - """Send notifications on build failure""" + """Send notifications on build failure.""" send_notifications.delay(self.version.pk, build_pk=self.build['id']) @app.task() def update_imported_docs(version_pk): """ - Check out or update the given project's repository + Check out or update the given project's repository. :param version_pk: Version id to update """ @@ -626,10 +631,11 @@ def update_imported_docs(version_pk): @app.task(queue='web') def sync_files(project_pk, version_pk, hostname=None, html=False, localmedia=False, search=False, pdf=False, epub=False): - """Sync build artifacts to application instances + """ + Sync build artifacts to application instances. - This task broadcasts from a build instance on build completion and - performs synchronization of build artifacts on each application instance. + This task broadcasts from a build instance on build completion and performs + synchronization of build artifacts on each application instance. """ # Clean up unused artifacts if not pdf: @@ -658,7 +664,8 @@ def sync_files(project_pk, version_pk, hostname=None, html=False, @app.task(queue='web') def move_files(version_pk, hostname, html=False, localmedia=False, search=False, pdf=False, epub=False): - """Task to move built documentation to web servers + """ + Task to move built documentation to web servers. :param version_pk: Version id to sync files for :param hostname: Hostname to sync to @@ -723,7 +730,8 @@ def move_files(version_pk, hostname, html=False, localmedia=False, search=False, @app.task(queue='web') def update_search(version_pk, commit, delete_non_commit_files=True): - """Task to update search indexes + """ + Task to update search indexes. :param version_pk: Version id to update :param commit: Commit that updated index @@ -814,7 +822,8 @@ def fileify(version_pk, commit): def _manage_imported_files(version, path, commit): - """Update imported files for version + """ + Update imported files for version. :param version: Version instance :param path: Path to search @@ -869,7 +878,8 @@ def send_notifications(version_pk, build_pk): def email_notification(version, build, email): - """Send email notifications for build failure + """ + Send email notifications for build failure. :param version: :py:class:`Version` instance that failed :param build: :py:class:`Build` instance that failed @@ -903,7 +913,8 @@ def email_notification(version, build, email): def webhook_notification(version, build, hook_url): - """Send webhook notification for project webhook + """ + Send webhook notification for project webhook. :param version: Version instance to send hook for :param build: Build instance that failed @@ -928,7 +939,8 @@ def webhook_notification(version, build, hook_url): @app.task(queue='web') def update_static_metadata(project_pk, path=None): - """Update static metadata JSON file + """ + Update static metadata JSON file. Metadata settings include the following project settings: @@ -979,8 +991,8 @@ def remove_dir(path): """ Remove a directory on the build/celery server. - This is mainly a wrapper around shutil.rmtree so that app servers - can kill things on the build server. + This is mainly a wrapper around shutil.rmtree so that app servers can kill + things on the build server. """ log.info("Removing %s", path) shutil.rmtree(path, ignore_errors=True) @@ -988,7 +1000,7 @@ def remove_dir(path): @app.task() def clear_artifacts(version_pk): - """Remove artifacts from the web servers""" + """Remove artifacts from the web servers.""" version = Version.objects.get(pk=version_pk) clear_pdf_artifacts(version) clear_epub_artifacts(version) @@ -1030,7 +1042,7 @@ def clear_html_artifacts(version): @app.task(queue='web') def sync_callback(_, version_pk, commit, *args, **kwargs): """ - This will be called once the sync_files tasks are done. + Called once the sync_files tasks are done. The first argument is the result from previous tasks, which we discard. """ diff --git a/readthedocs/projects/utils.py b/readthedocs/projects/utils.py index 2683ec59cfb..d141ee56a6c 100644 --- a/readthedocs/projects/utils.py +++ b/readthedocs/projects/utils.py @@ -1,4 +1,4 @@ -"""Utility functions used by projects""" +"""Utility functions used by projects.""" from __future__ import absolute_import @@ -32,7 +32,8 @@ def version_from_slug(slug, version): def find_file(filename): - """Recursively find matching file from the current working path + """ + Recursively find matching file from the current working path. :param file: Filename to match :returns: A list of matching filenames. @@ -45,7 +46,8 @@ def find_file(filename): def run(*commands): - """Run one or more commands + """ + Run one or more commands. Each argument in `commands` can be passed as a string or as a list. Passing as a list is the preferred method, as space escaping is more explicit and it @@ -106,7 +108,8 @@ def run(*commands): def safe_write(filename, contents): - """Normalize and write to filename + """ + Normalize and write to filename. Write ``contents`` to the given ``filename``. If the filename's directory does not exist, it is created. Contents are written as UTF-8, diff --git a/readthedocs/projects/views/mixins.py b/readthedocs/projects/views/mixins.py index 4c450d0f18d..65b6a43b64c 100644 --- a/readthedocs/projects/views/mixins.py +++ b/readthedocs/projects/views/mixins.py @@ -1,4 +1,4 @@ -"""Mixin classes for project views""" +"""Mixin classes for project views.""" from __future__ import absolute_import from builtins import object @@ -9,7 +9,8 @@ class ProjectRelationMixin(object): - """Mixin class for constructing model views for project dashboard + """ + Mixin class for constructing model views for project dashboard. This mixin class is used for model views on models that have a relation to the :py:cls:`Project` model. diff --git a/readthedocs/projects/views/private.py b/readthedocs/projects/views/private.py index cf4cac0e968..39b0b14f0ff 100644 --- a/readthedocs/projects/views/private.py +++ b/readthedocs/projects/views/private.py @@ -1,4 +1,4 @@ -"""Project views for authenticated users""" +"""Project views for authenticated users.""" from __future__ import absolute_import import logging @@ -52,7 +52,7 @@ class PrivateViewMixin(LoginRequiredMixin): class ProjectDashboard(PrivateViewMixin, ListView): - """Project dashboard""" + """Project dashboard.""" model = Project template_name = 'projects/project_dashboard.html' @@ -75,7 +75,8 @@ def get_context_data(self, **kwargs): @login_required def project_manage(__, project_slug): - """Project management view + """ + Project management view. Where you will have links to edit the projects' configuration, edit the files associated with that project, etc. @@ -131,7 +132,8 @@ def get_success_url(self): @login_required def project_versions(request, project_slug): - """Project versions view + """ + Project versions view. Shows the available versions and lets the user choose which ones he would like to have built. @@ -161,7 +163,7 @@ def project_versions(request, project_slug): @login_required def project_version_detail(request, project_slug, version_slug): - """Project version detail page""" + """Project version detail page.""" project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) version = get_object_or_404( Version.objects.public(user=request.user, project=project, only_active=False), @@ -189,7 +191,8 @@ def project_version_detail(request, project_slug, version_slug): @login_required def project_delete(request, project_slug): - """Project delete confirmation view + """ + Project delete confirmation view. Make a project as deleted on POST, otherwise show a form asking for confirmation of delete. @@ -213,14 +216,14 @@ def project_delete(request, project_slug): class ImportWizardView(ProjectSpamMixin, PrivateViewMixin, SessionWizardView): - """Project import wizard""" + """Project import wizard.""" form_list = [('basics', ProjectBasicsForm), ('extra', ProjectExtraForm)] condition_dict = {'extra': lambda self: self.is_advanced()} def get_form_kwargs(self, step=None): - """Get args to pass into form instantiation""" + """Get args to pass into form instantiation.""" kwargs = {} kwargs['user'] = self.request.user if step == 'basics': @@ -228,11 +231,12 @@ def get_form_kwargs(self, step=None): return kwargs def get_template_names(self): - """Return template names based on step name""" + """Return template names based on step name.""" return 'projects/import_{0}.html'.format(self.steps.current) def done(self, form_list, **kwargs): - """Save form data as object instance + """ + Save form data as object instance. Don't save form data directly, instead bypass documentation building and other side effects for now, by signalling a save without commit. Then, @@ -260,14 +264,14 @@ def done(self, form_list, **kwargs): args=[project.slug])) def is_advanced(self): - """Determine if the user selected the `show advanced` field""" + """Determine if the user selected the `show advanced` field.""" data = self.get_cleaned_data_for_step('basics') or {} return data.get('advanced', True) class ImportDemoView(PrivateViewMixin, View): - """View to pass request on to import form to import demo project""" + """View to pass request on to import form to import demo project.""" form_class = ProjectBasicsForm request = None @@ -275,7 +279,7 @@ class ImportDemoView(PrivateViewMixin, View): kwargs = None def get(self, request, *args, **kwargs): - """Process link request as a form post to the project import form""" + """Process link request as a form post to the project import form.""" self.request = request self.args = args self.kwargs = kwargs @@ -305,7 +309,7 @@ def get(self, request, *args, **kwargs): args=[project.slug])) def get_form_data(self): - """Get form data to post to import form""" + """Get form data to post to import form.""" return { 'name': '{0}-demo'.format(self.request.user.username), 'repo_type': 'git', @@ -313,13 +317,14 @@ def get_form_data(self): } def get_form_kwargs(self): - """Form kwargs passed in during instantiation""" + """Form kwargs passed in during instantiation.""" return {'user': self.request.user} class ImportView(PrivateViewMixin, TemplateView): - """On GET, show the source an import view, on POST, mock out a wizard + """ + On GET, show the source an import view, on POST, mock out a wizard. If we are accepting POST data, use the fields to seed the initial data in :py:class:`ImportWizardView`. The import templates will redirect the form to @@ -330,7 +335,8 @@ class ImportView(PrivateViewMixin, TemplateView): wizard_class = ImportWizardView def get(self, request, *args, **kwargs): - """Display list of repositories to import + """ + Display list of repositories to import. Adds a warning to the listing if any of the accounts connected for the user are not supported accounts. @@ -377,7 +383,7 @@ def get_context_data(self, **kwargs): @login_required def edit_alias(request, project_slug, alias_id=None): - """Edit project alias form view""" + """Edit project alias form view.""" proj = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) if alias_id: alias = proj.aliases.get(pk=alias_id) @@ -455,7 +461,7 @@ def get(self, request, *args, **kwargs): @login_required def project_users(request, project_slug): - """Project users view and form view""" + """Project users view and form view.""" project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) @@ -490,7 +496,7 @@ def project_users_delete(request, project_slug): @login_required def project_notifications(request, project_slug): - """Project notification view and form view""" + """Project notification view and form view.""" project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) @@ -538,7 +544,7 @@ def project_comments_settings(request, project_slug): @login_required def project_notifications_delete(request, project_slug): - """Project notifications delete confirmation view""" + """Project notifications delete confirmation view.""" if request.method != 'POST': return HttpResponseNotAllowed('Only POST is allowed') project = get_object_or_404(Project.objects.for_admin_user(request.user), @@ -556,7 +562,7 @@ def project_notifications_delete(request, project_slug): @login_required def project_translations(request, project_slug): - """Project translations view and form view""" + """Project translations view and form view.""" project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) form = TranslationForm(data=request.POST or None, parent=project) @@ -587,7 +593,7 @@ def project_translations_delete(request, project_slug, child_slug): @login_required def project_redirects(request, project_slug): - """Project redirects view and form view""" + """Project redirects view and form view.""" project = get_object_or_404(Project.objects.for_admin_user(request.user), slug=project_slug) @@ -609,7 +615,7 @@ def project_redirects(request, project_slug): @login_required def project_redirects_delete(request, project_slug): - """Project redirect delete view""" + """Project redirect delete view.""" if request.method != 'POST': return HttpResponseNotAllowed('Only POST is allowed') project = get_object_or_404(Project.objects.for_admin_user(request.user), @@ -626,7 +632,8 @@ def project_redirects_delete(request, project_slug): @login_required def project_version_delete_html(request, project_slug, version_slug): - """Project version 'delete' HTML + """ + Project version 'delete' HTML. This marks a version as not built """ @@ -672,7 +679,7 @@ class DomainDelete(DomainMixin, DeleteView): class IntegrationMixin(ProjectAdminMixin, PrivateViewMixin): - """Project external service mixin for listing webhook objects""" + """Project external service mixin for listing webhook objects.""" model = Integration integration_url_field = 'integration_pk' @@ -689,7 +696,7 @@ def get_integration_queryset(self): return self.model.objects.filter(project=self.project) def get_integration(self): - """Return project integration determined by url kwarg""" + """Return project integration determined by url kwarg.""" if self.integration_url_field not in self.kwargs: return None return get_object_or_404( @@ -765,7 +772,8 @@ def get_object(self): class IntegrationWebhookSync(IntegrationMixin, GenericView): - """Resync a project webhook + """ + Resync a project webhook. The signal will add a success/failure message on the request. """ diff --git a/readthedocs/projects/views/public.py b/readthedocs/projects/views/public.py index e5d856c181f..f86360a6621 100644 --- a/readthedocs/projects/views/public.py +++ b/readthedocs/projects/views/public.py @@ -1,4 +1,4 @@ -"""Public project views""" +"""Public project views.""" from __future__ import absolute_import from collections import OrderedDict @@ -38,7 +38,7 @@ class ProjectIndex(ListView): - """List view of public :py:class:`Project` instances""" + """List view of public :py:class:`Project` instances.""" model = Project @@ -70,7 +70,7 @@ def get_context_data(self, **kwargs): class ProjectDetailView(BuildTriggerMixin, ProjectOnboardMixin, DetailView): - """Display project onboard steps""" + """Display project onboard steps.""" model = Project slug_url_kwarg = 'project_slug' @@ -106,7 +106,7 @@ def get_context_data(self, **kwargs): @never_cache def project_badge(request, project_slug): - """Return a sweet badge for the project""" + """Return a sweet badge for the project.""" badge_path = "projects/badges/%s.svg" version_slug = request.GET.get('version', LATEST) try: @@ -128,7 +128,7 @@ def project_badge(request, project_slug): def project_downloads(request, project_slug): - """A detail view for a project with various dataz""" + """A detail view for a project with various dataz.""" project = get_object_or_404(Project.objects.protected(request.user), slug=project_slug) versions = Version.objects.public(user=request.user, project=project) version_data = OrderedDict() @@ -158,7 +158,6 @@ def project_download_media(request, project_slug, type_, version_slug): .. warning:: This is linked directly from the HTML pages. It should only care about the Version permissions, not the actual Project permissions. - """ version = get_object_or_404( Version.objects.public(user=request.user), @@ -189,7 +188,7 @@ def project_download_media(request, project_slug, type_, version_slug): def search_autocomplete(request): - """Return a json list of project names""" + """Return a json list of project names.""" if 'term' in request.GET: term = request.GET['term'] else: @@ -208,7 +207,7 @@ def search_autocomplete(request): def version_autocomplete(request, project_slug): - """Return a json list of version names""" + """Return a json list of version names.""" queryset = Project.objects.public(request.user) get_object_or_404(queryset, slug=project_slug) versions = Version.objects.public(request.user) @@ -247,7 +246,7 @@ def version_filter_autocomplete(request, project_slug): def file_autocomplete(request, project_slug): - """Return a json list of file names""" + """Return a json list of file names.""" if 'term' in request.GET: term = request.GET['term'] else: @@ -266,7 +265,7 @@ def file_autocomplete(request, project_slug): def elastic_project_search(request, project_slug): - """Use elastic search to search in a project""" + """Use elastic search to search in a project.""" queryset = Project.objects.protected(request.user) project = get_object_or_404(queryset, slug=project_slug) version_slug = request.GET.get('version', LATEST) @@ -340,7 +339,8 @@ def elastic_project_search(request, project_slug): def project_versions(request, project_slug): - """Project version list view + """ + Project version list view. Shows the available versions and lets the user choose which ones to build. """ @@ -371,7 +371,7 @@ def project_versions(request, project_slug): def project_analytics(request, project_slug): - """Have a analytics API placeholder""" + """Have a analytics API placeholder.""" project = get_object_or_404(Project.objects.protected(request.user), slug=project_slug) analytics_cache = cache.get('analytics:%s' % project_slug) @@ -416,7 +416,7 @@ def project_analytics(request, project_slug): def project_embed(request, project_slug): - """Have a content API placeholder""" + """Have a content API placeholder.""" project = get_object_or_404(Project.objects.protected(request.user), slug=project_slug) version = project.versions.get(slug=LATEST) diff --git a/readthedocs/redirects/utils.py b/readthedocs/redirects/utils.py index e912b8e792a..1edc628626a 100644 --- a/readthedocs/redirects/utils.py +++ b/readthedocs/redirects/utils.py @@ -1,11 +1,11 @@ -"""Redirection view support. +""" +Redirection view support. This module allows for parsing a URL path, looking up redirects associated with it in the database, and generating a redirect response. These are not used directly as views; they are instead included into 404 handlers, so that redirects only take effect if no other view matches. - """ from __future__ import absolute_import from django.http import HttpResponseRedirect @@ -20,7 +20,8 @@ def project_and_path_from_request(request, path): - """Parse the project from a request path. + """ + Parse the project from a request path. Return a tuple (project, path) where `project` is a projects.Project if a matching project exists, and `path` is the unmatched remainder of the @@ -28,7 +29,6 @@ def project_and_path_from_request(request, path): If the path does not match, or no matching project is found, then `project` will be ``None``. - """ if hasattr(request, 'slug'): project_slug = request.slug diff --git a/readthedocs/restapi/permissions.py b/readthedocs/restapi/permissions.py index a6a5abc58c9..615872d307e 100644 --- a/readthedocs/restapi/permissions.py +++ b/readthedocs/restapi/permissions.py @@ -62,12 +62,13 @@ def has_object_permission(self, request, view, obj): class APIRestrictedPermission(permissions.BasePermission): - """Allow admin write, authenticated and anonymous read only + """ + Allow admin write, authenticated and anonymous read only. - This differs from :py:class:`APIPermission` by not allowing for authenticated - POSTs. This permission is endpoints like ``/api/v2/build/``, which are used - by admin users to coordinate build instance creation, but only should be - readable by end users. + This differs from :py:class:`APIPermission` by not allowing for + authenticated POSTs. This permission is endpoints like ``/api/v2/build/``, + which are used by admin users to coordinate build instance creation, but + only should be readable by end users. """ def has_permission(self, request, view): diff --git a/readthedocs/restapi/serializers.py b/readthedocs/restapi/serializers.py index b85f2373136..63616ea19d8 100644 --- a/readthedocs/restapi/serializers.py +++ b/readthedocs/restapi/serializers.py @@ -28,7 +28,8 @@ class Meta(object): class ProjectAdminSerializer(ProjectSerializer): - """Project serializer for admin only access + """ + Project serializer for admin only access. Includes special internal fields that don't need to be exposed through the general API, mostly for fields used in the build process @@ -77,7 +78,7 @@ class Meta(object): class VersionAdminSerializer(VersionSerializer): - """Version serializer that returns admin project data""" + """Version serializer that returns admin project data.""" project = ProjectAdminSerializer() @@ -93,7 +94,7 @@ class Meta(object): class BuildSerializer(serializers.ModelSerializer): - """Build serializer for user display, doesn't display internal fields""" + """Build serializer for user display, doesn't display internal fields.""" commands = BuildCommandSerializer(many=True, read_only=True) state_display = serializers.ReadOnlyField(source='get_state_display') @@ -105,7 +106,7 @@ class Meta(object): class BuildAdminSerializer(BuildSerializer): - """Build serializer for display to admin users and build instances""" + """Build serializer for display to admin users and build instances.""" class Meta(BuildSerializer.Meta): exclude = () @@ -142,7 +143,7 @@ class Meta(object): class RemoteRepositorySerializer(serializers.ModelSerializer): - """Remote service repository serializer""" + """Remote service repository serializer.""" organization = RemoteOrganizationSerializer() matches = serializers.SerializerMethodField() diff --git a/readthedocs/restapi/utils.py b/readthedocs/restapi/utils.py index 4f655f8ffdc..b80190bc427 100644 --- a/readthedocs/restapi/utils.py +++ b/readthedocs/restapi/utils.py @@ -78,7 +78,8 @@ def delete_versions(project, version_data): def index_search_request(version, page_list, commit, project_scale, page_scale, section=True, delete=True): - """Update search indexes with build output JSON + """ + Update search indexes with build output JSON. In order to keep sub-projects all indexed on the same shard, indexes will be updated using the parent project's slug as the routing value. diff --git a/readthedocs/restapi/views/integrations.py b/readthedocs/restapi/views/integrations.py index 82feb9f8bfa..80699a1c196 100644 --- a/readthedocs/restapi/views/integrations.py +++ b/readthedocs/restapi/views/integrations.py @@ -39,7 +39,7 @@ class WebhookMixin(object): integration_type = None def post(self, request, project_slug): - """Set up webhook post view with request and project objects""" + """Set up webhook post view with request and project objects.""" self.request = request self.project = None try: @@ -57,7 +57,7 @@ def get_project(self, **kwargs): return Project.objects.get(**kwargs) def finalize_response(self, req, *args, **kwargs): - """If the project was set on POST, store an HTTP exchange""" + """If the project was set on POST, store an HTTP exchange.""" resp = super(WebhookMixin, self).finalize_response(req, *args, **kwargs) if hasattr(self, 'project') and self.project: HttpExchange.objects.from_exchange( @@ -69,15 +69,16 @@ def finalize_response(self, req, *args, **kwargs): return resp def get_data(self): - """Normalize posted data""" + """Normalize posted data.""" return normalize_request_payload(self.request) def handle_webhook(self): - """Handle webhook payload""" + """Handle webhook payload.""" raise NotImplementedError def get_integration(self): - """Get or create an inbound webhook to track webhook requests + """ + Get or create an inbound webhook to track webhook requests. We shouldn't need this, but to support legacy webhooks, we can't assume that a webhook has ever been created on our side. Most providers don't @@ -97,7 +98,8 @@ def get_integration(self): return integration def get_response_push(self, project, branches): - """Build branches on push events and return API response + """ + Build branches on push events and return API response. Return a JSON response with the following:: @@ -124,7 +126,8 @@ def get_response_push(self, project, branches): class GitHubWebhookView(WebhookMixin, APIView): - """Webhook consumer for GitHub + """ + Webhook consumer for GitHub. Accepts webhook events from GitHub, 'push' events trigger builds. Expects the webhook event type will be included in HTTP header ``X-GitHub-Event``, and @@ -164,7 +167,8 @@ def handle_webhook(self): class GitLabWebhookView(WebhookMixin, APIView): - """Webhook consumer for GitLab + """ + Webhook consumer for GitLab. Accepts webhook events from GitLab, 'push' events trigger builds. @@ -195,7 +199,8 @@ def handle_webhook(self): class BitbucketWebhookView(WebhookMixin, APIView): - """Webhook consumer for Bitbucket + """ + Webhook consumer for Bitbucket. Accepts webhook events from Bitbucket, 'repo:push' events trigger builds. @@ -236,7 +241,8 @@ def handle_webhook(self): class IsAuthenticatedOrHasToken(permissions.IsAuthenticated): - """Allow authenticated users and requests with token auth through + """ + Allow authenticated users and requests with token auth through. This does not check for instance-level permissions, as the check uses methods from the view to determine if the token matches. @@ -250,7 +256,8 @@ def has_permission(self, request, view): class APIWebhookView(WebhookMixin, APIView): - """API webhook consumer + """ + API webhook consumer. Expects the following JSON:: @@ -263,7 +270,8 @@ class APIWebhookView(WebhookMixin, APIView): permission_classes = [IsAuthenticatedOrHasToken] def get_project(self, **kwargs): - """Get authenticated user projects, or token authed projects + """ + Get authenticated user projects, or token authed projects. Allow for a user to either be authed to receive a project, or require the integration token to be specified as a POST argument. @@ -304,7 +312,8 @@ def handle_webhook(self): class WebhookView(APIView): - """This is the main webhook view for webhooks with an ID + """ + Main webhook view for webhooks with an ID. The handling of each view is handed off to another view. This should only ever get webhook requests for established webhooks on our side. The other @@ -320,7 +329,7 @@ class WebhookView(APIView): } def post(self, request, project_slug, integration_pk): - """Set up webhook post view with request and project objects""" + """Set up webhook post view with request and project objects.""" integration = get_object_or_404( Integration, project__slug=project_slug, diff --git a/readthedocs/restapi/views/model_views.py b/readthedocs/restapi/views/model_views.py index a4e997d4097..24d99980fb2 100644 --- a/readthedocs/restapi/views/model_views.py +++ b/readthedocs/restapi/views/model_views.py @@ -34,7 +34,8 @@ class UserSelectViewSet(viewsets.ModelViewSet): - """View set that varies serializer class based on request user credentials + """ + View set that varies serializer class based on request user credentials. Viewsets using this class should have an attribute `admin_serializer_class`, which is a serializer that might have more fields that only admin/staff @@ -50,7 +51,7 @@ def get_serializer_class(self): return self.serializer_class def get_queryset(self): - """Use our API manager method to determine authorization on queryset""" + """Use our API manager method to determine authorization on queryset.""" return self.model.objects.api(self.request.user) @@ -129,9 +130,10 @@ def canonical_url(self, request, **kwargs): }) @decorators.detail_route(permission_classes=[permissions.IsAdminUser], methods=['post']) - def sync_versions(self, request, **kwargs): + def sync_versions(self, request, **kwargs): # noqa: D205 """ - Sync the version data in the repo (on the build server) with what we have in the database. + Sync the version data in the repo (on the build server) with what we + have in the database. Returns the identifiers for the versions that have been deleted. """ diff --git a/readthedocs/restapi/views/search_views.py b/readthedocs/restapi/views/search_views.py index febef2ac5b4..abe36174097 100644 --- a/readthedocs/restapi/views/search_views.py +++ b/readthedocs/restapi/views/search_views.py @@ -21,7 +21,7 @@ @decorators.permission_classes((permissions.IsAdminUser,)) @decorators.renderer_classes((JSONRenderer,)) def index_search(request): - """Add things to the search index""" + """Add things to the search index.""" data = request.data['data'] version_pk = data['version_pk'] commit = data.get('commit') @@ -41,7 +41,7 @@ def index_search(request): @decorators.permission_classes((permissions.AllowAny,)) @decorators.renderer_classes((JSONRenderer,)) def search(request): - """Perform search, supplement links by resolving project domains""" + """Perform search, supplement links by resolving project domains.""" project_slug = request.GET.get('project', None) version_slug = request.GET.get('version', LATEST) query = request.GET.get('q', None) @@ -100,7 +100,8 @@ def project_search(request): @decorators.permission_classes((permissions.AllowAny,)) @decorators.renderer_classes((JSONRenderer,)) def section_search(request): - """Section search + """ + Section search. Queries with query ``q`` across all documents and projects. Queries can be limited to a single project or version by using the ``project`` and @@ -129,7 +130,6 @@ def section_search(request): Example:: GET /api/v2/search/section/?q=virtualenv&project=django - """ query = request.GET.get('q', None) if not query: diff --git a/readthedocs/rtd_tests/base.py b/readthedocs/rtd_tests/base.py index 054d16979df..c30f148d6d5 100644 --- a/readthedocs/rtd_tests/base.py +++ b/readthedocs/rtd_tests/base.py @@ -37,20 +37,22 @@ def tearDown(self): @patch('readthedocs.projects.views.private.trigger_build', lambda x, basic: None) class MockBuildTestCase(TestCase): - """Mock build triggers for test cases""" + """Mock build triggers for test cases.""" pass class RequestFactoryTestMixin(object): - """Adds helper methods for testing with :py:class:`RequestFactory` + """ + Adds helper methods for testing with :py:class:`RequestFactory` This handles setting up authentication, messages, and session handling """ def request(self, *args, **kwargs): - """Perform request from factory + """ + Perform request from factory. :param method: Request method as string :returns: Request instance @@ -88,7 +90,7 @@ def request(self, *args, **kwargs): class WizardTestCase(RequestFactoryTestMixin, TestCase): - """Test case for testing wizard forms""" + """Test case for testing wizard forms.""" step_data = OrderedDict({}) url = None @@ -97,7 +99,8 @@ class WizardTestCase(RequestFactoryTestMixin, TestCase): @patch('readthedocs.projects.views.private.trigger_build', lambda x, basic: None) def post_step(self, step, **kwargs): - """Post step form data to `url`, using supplementary `kwargs` + """ + Post step form data to `url`, using supplementary `kwargs` Use data from kwargs to build dict to pass into form """ @@ -121,7 +124,7 @@ def post_step(self, step, **kwargs): # We use camelCase on purpose here to conform with unittest's naming # conventions. def assertWizardResponse(self, response, step=None): # noqa - """Assert successful wizard response""" + """Assert successful wizard response.""" # This is the last form if step is None: try: @@ -149,7 +152,8 @@ def assertWizardResponse(self, response, step=None): # noqa # We use camelCase on purpose here to conform with unittest's naming # conventions. def assertWizardFailure(self, response, field, match=None): # noqa - """Assert field threw a validation error + """ + Assert field threw a validation error. response Client response object diff --git a/readthedocs/search/lib.py b/readthedocs/search/lib.py index 994601032d7..8500a829b03 100644 --- a/readthedocs/search/lib.py +++ b/readthedocs/search/lib.py @@ -15,7 +15,7 @@ def search_project(request, query, language=None): - """Search index for projects matching query""" + """Search index for projects matching query.""" body = { "query": { "bool": { @@ -50,7 +50,8 @@ def search_project(request, query, language=None): def search_file(request, query, project_slug=None, version_slug=LATEST, taxonomy=None): - """Search index for files matching query + """ + Search index for files matching query. Raises a 404 error on missing project @@ -163,7 +164,8 @@ def search_file(request, query, project_slug=None, version_slug=LATEST, taxonomy def search_section(request, query, project_slug=None, version_slug=LATEST, path=None): - """Search for a section of content + """ + Search for a section of content. When you search, you will have a ``project`` facet, which includes the number of matching sections per project. When you search inside a project, diff --git a/readthedocs/search/utils.py b/readthedocs/search/utils.py index 385e1110804..139c6b5e976 100644 --- a/readthedocs/search/utils.py +++ b/readthedocs/search/utils.py @@ -18,7 +18,7 @@ def process_mkdocs_json(version, build_dir=True): - """Given a version object, return a list of page dicts from disk content""" + """Given a version object, return a list of page dicts from disk content.""" if build_dir: full_path = version.project.full_json_path(version.slug) else: @@ -215,7 +215,8 @@ def parse_sphinx_sections(content): def parse_mkdocs_sections(content): - """Generate a list of sections from mkdocs-style html. + """ + Generate a list of sections from mkdocs-style html. May raise a ValueError """ diff --git a/requirements/lint.txt b/requirements/lint.txt index fb39b4bdf50..5802fb2cc86 100644 --- a/requirements/lint.txt +++ b/requirements/lint.txt @@ -1,8 +1,13 @@ -r pip.txt maxcdn astroid -pylint -# prospector==0.12.6 currently has issues with pydocstyle -prospector==0.12.5 + +# pylint 1.8.0 is having problems: +# File "/home/humitos/.pyenv/versions/2.7.14/envs/pylint/lib/python2.7/site-packages/pylint_django/plugin.py", line 22, in register +# start = name_checker.config.const_rgx.pattern[:-2] +# AttributeError: 'NoneType' object has no attribute 'pattern +pylint==1.7.5 + +prospector pylint-django pyflakes