Repository: oppia/oppia

Issue:
Update docstrings in the Python backend code to follow the style guide.
We've been a bit lax about adding docstrings to code in the backend. Instead, we should be following the style guide: https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments -- docstrings should define args, return values, exceptions, etc. The aim of this issue is to ensure that the backend code is properly documented; we will tackle this on a file-by-file basis.

Note: if you're a new contributor, this issue is a particularly good introductory one to tackle, since it will require reading, understanding and documenting the purpose of a file in the codebase, and help you get acquainted with how the Oppia backend is structured. It's important that you're able to communicate and write clearly, though!

Some useful tips:
-  Please follow our [style guide](https://github.com/oppia/oppia/wiki/Coding-style-guide#general) (and also the [Google Python style guide](https://google.github.io/styleguide/pyguide.html?showone=Comments#Comments)) when writing docstrings. Make sure to follow the patterns established there as closely as possible, including spacing, capitalization and punctuation. Also, it's a good idea to look at some of the existing files (see below) to get a sense of the correct format -- we recommend following this closely in order to maintain uniformity across the codebase.
- If applicable, make sure that the docstrings for all functions correctly specify the args, return values, etc. If a function has no args, you can omit the Args section; if it has no return value, you can omit the Returns section.
- If a docstring is not present in the code, or the existing one is too vague for you to understand what's going on just by looking at it, then it should be rewritten to be more clear. You'll often need to read the code as well, and do a grep on the callsites to see how it's being used, in order to get a clear understanding of what it's doing. This is good practice for learning how to navigate the codebase, and is an intended part of this project.
- If you feel that parts of the codebase are unclear or incorrectly factored, please feel free to bring this up! The whole point of docstrings is to improve code health and accessibility for new developers, so if you spot other things that can be done in service of that aim, we should also look into doing that.

**Completed:**
- [x] core.controllers.base @shaz13 
- [x] core.controllers.cron @saeedjassani
- [x] core.controllers.reader @96mohitm
- [x] core.domain.activity_domain @andromfins
- [x] core.domain.activity_services @souravsingh
- [x] core.domain.classifier_services @andromfins 
- [x] core.domain.collection_domain @domevit
- [x] core.domain.collection_services @kahkhang 
- [x] core.domain.config_services @vibhor98 
- [x] core.domain.email_manager @Oishikatta 
- [x] core.domain.exp_domain @domevit
- [x] core.domain.exp_services @DubeySandeep
- [x] core.domain.feedback_domain @terrameijar 
- [x] core.domain.feedback_jobs_continuous
- [x] core.domain.feedback_services @andimeo
- [x] core.domain.fs_domain @kartik-25 
- [x] core.domain.html_cleaner @seanlip
- [x] core.domain.param_domain @rohan-gulati
- [x] core.domain.rating_services @BojOnTheBeat 
- [x] core.domain.rights_manager @ayejay
- [x] core.domain.stats_domain @Arunabh98
- [x] core.domain.stats_jobs_continuous @Arunabh98 
- [x] core.domain.stats_jobs_one_off @zpuller
- [x] core.domain.stats_services @manoj-makkuboy 
- [x] core.domain.subscription_services @vibhor98 
- [x] core.domain.summary_services @domevit 
- [x] core.domain.user_jobs_continuous @shubha1593 
- [x] core.domain.user_services @Nalinc 
- [x] core.domain.value_generators_domain @meetsha 
- [x] core.jobs @brianrodri
- [x] core.platform.app_identity.gae_app_identity_services @ubhisat
- [x] core.platform.models @pranavsid98 
- [x] core.platform.transactions.gae_transaction_services @ubhisat
- [x] core.storage.base_model.gae_models @vojtechjelinek
- [x] core.storage.collection.gae_models @kaggis
- [x] core.storage.email.gae_models @rajan-garg
- [x] core.storage.exploration.gae_models @kirankonduru
- [x] core.storage.feedback.gae_models @naveenshukla
- [x] core.storage.file.gae_models @Ian91 
- [x] core.storage.job.gae_models @sasacocic
- [x] core.storage.statistics.gae_models @abhishekarya286
- [x] core.storage.user.gae_models @Anthony-Alridge 
- [x] core.tests.test_util_jobs @seanlip
- [x] core.tests.test_utils @vibhor98
- [x] utils @yashLadha 


Discussion:
@seanlip I would like to work in the issue.

Hi @souravsingh, do you think you could finish up #2051 first, please? Thanks! 

@seanlip I would like to work on this issue.

Hi @andromfins -- now that you've had a go at writing docstrings for a single class, would you mind picking one or two other files and doing the same thing for those? Let me know which ones you pick, so that I can mark you off against them.

Thanks!

Hi, i'm working on **core.domain.classifier_services**.

OK, thanks @andromfins! 

@seanlip Can I take a stab at other file to add docstrings?

Yep, sounds good. Could you take core.domain.html_cleaner? Thanks!

@andromfins -- are you still working on this?

  @seanlip Yes!

I would like to work on `core.domain.stats_domain`

@Arunabh98 assigned you on `core.domain.stats_domain`

Almost completed `core.domain.stats_domain`. Now working on `core.domain.stats_jobs_continuous`

hi @seanlip 
I am working on this issue.
I am writing the docstrings for /core/storage/collection/gae_models.py as i was assigned as a starting project.

@seanlip can I work on core.controllers.cron?
Yep, sounds good! I've updated the list.
@seanlip I'd like to work on core.storage.job.gae_models.
Sounds good, thanks @sasacocic! I've updated the list. 
@seanlip I'd like to work on core.platform.models. Thanks

@seanlip I'd like to work on core.domain.stats_services
Sounds good, thanks @arjunmahishi! I've assigned you. 
@seanlip I'd like to work on core.domain.summary_services
Sounds good, thanks! I've assigned you.
@seanlip Hello, i would work on core.domain.config_domain if possible.
@tvallois I have assigned you to that. Thanks
@seanlip I can do core.domain.rights_manager.py
Sounds good @fodra, I've assigned you. Thanks! 
@seanlip I would like to take _core.tests.test_util_jobs_
Sounds great, I've assigned you. Thanks @terrameijar! 
@seanlip could i work on  core.storage.statistics.gae_models?

I'd like to contribute to this, could you please assign me one of the sub tasks?
Hi @fsam95, welcome to Oppia! Could you please follow the instructions [here](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up) first?

@cosmicAsymmetry The list of tasks that are yet to be done is in the "Not started" section. Would you be up for taking a look at the corresponding files, and picking one that looks approachable? Note that test_utils has some prior work that you might be able to extend, though it's slightly unusual because the functions within it are meant mainly for supporting backend testing (and are therefore not directly user-facing). Thanks!
I've followed the instructions :). 
Thanks @fsam95! Assigned you. 
@seanlip I want to take one of the files but it looks like all the files in "Not started" section have been documented? Can you assign me to one which has not been taken? Thanks! :)
Hi @zhengxit -- actually, all those files still need work, I think, be it improving the existing docstrings, writing new docstrings for methods/functions which don't have them, or making sure that everything follows the correct style. So, please feel free to pick one of them!
(P.S. If you see a particular file in the "Not started" section that you're not sure what to do with, just ask.)
@seanlip I would like to take **core.controllers.base**

Assigned. Thanks @shaz13! 
I'm starting work on **core.jobs**.
@seanlip Please assign me an issue.I also want to contribute.
can I take #2394 Docstrings for controllers/base.py
@seanlip I am taking core.domain.stats_services
OK, thanks! Assigned you.
hey...I am working on core.domain.stats_services....I can see previous docstrings in it.So, what exactly is incomplete in this project.
Hello @arjunsuri, you can look at the previous PRs' ( alike #3448, #2459 ) to get an idea about expected format. I looked into the file [core.domain.stats_services. ](https://github.com/oppia/oppia/blob/develop/core/domain/stats_services.py) there are still few functions missing Args, Return information which you should document in your PR. If you still have any doubts feel free to ping us in the Gitter channel. 
I would like to work on `core.domain.value_generators_domain`
Sounds good, thanks @meetsha. I've assigned you! 
I would like to work on  `core.domain.user_jobs_continuous`

Sounds good, thanks @shubha1593. I've assigned you! 
I would like to be assigned core.domain.fs_domain :)
Thanks @Wert1996. I've assigned you!
@seanlip I want to take core.domain.stats_services

@arjunsuri, I've assigned you!
@kevinlee12  I was looking into core.domain.stats_services....Can you please tell me what exactly I have to do with documentation as this is my first ever contribution
To start off, I'd recommend taking a look at the other PR's like PR #3448, there's quite a lot of PR's linked so you can use those as samples of what we are looking for!
Actually...I think I am not able to understand the complete code....otherwise I could have done it by now.Can you please help me in any way so that I can understand the written code and not the docstring...so that I can write my own docstring. 
@arjunsuri Hi there! I suggest you using Sublime Text editor or something similar (VS Code, Atom) to look up the definitions throughout the codebase and what they do. If you have any particular question about a function and its use then feel free to ping us on Gitter. Thanks!
There are no left out files for docstrings....whwre should I start? Any suggestions...
@seanlip I would like to work on the parts that are left
Hi @adsrc, sounds good, thanks! Would you mind following the instructions [here](https://github.com/oppia/oppia/wiki/Contributing-code-to-Oppia#setting-things-up) to get started? Once you've done that, feel free to pick a file on this issue and tell us what it is, and we'll update the thread accordingly. Thanks!
Hi **@seanlip**, I would like to work on `core.domain.config_services.py`. 
Hi @vibhor98, sounds good -- I've assigned you. Thanks! 
Hi @seanlip, I would like to work on ` core.domain.fs_domain` 

Hi @kartik-25, sure, assigned you!

Resolving patch:
diff --git a/core/domain/fs_domain.py b/core/domain/fs_domain.py
--- a/core/domain/fs_domain.py
+++ b/core/domain/fs_domain.py
@@ -34,8 +34,17 @@
 
 
 class FileMetadata(object):
-    """A class representing the metadata of a file."""
+    """A class representing the metadata of a file.
+
+    Attributes:
+        size: int. The size of the file, in bytes.
+    """
     def __init__(self, metadata):
+        """Constructs a FileMetadata object.
+
+        Args:
+            metadata: FileMetadataModel. The file metadata model instance.
+        """
         self._size = metadata.size if (metadata is not None) else None
 
     @property
@@ -44,16 +53,32 @@ def size(self):
 
 
 class FileStreamWithMetadata(object):
-    """A class that wraps a file stream, but adds extra attributes to it."""
+    """A class that wraps a file stream, but adds extra attributes to it.
+
+    Attributes:
+        content: str. The content of the file snapshot.
+        version: int. The version number of the file.
+        metadata: FileMetadata. The file metadata domain instance.
+    """
 
     def __init__(self, content, version, metadata):
-        """The args are a file content blob and a metadata model object."""
+        """Constructs a FileStreamWithMetadata object.
+
+        Args:
+            content: str. The content of the file snapshots.
+            version: int. The version number of the file.
+            metadata: FileMetadataModel. The file metadata model instance.
+        """
         self._content = content
         self._version = version
         self._metadata = FileMetadata(metadata)
 
     def read(self):
-        """Emulates stream.read(). Returns all bytes and emulates EOF."""
+        """Emulates stream.read(). Returns all bytes and emulates EOF.
+
+        Returns:
+            content: str. The content of the file snapshot.
+        """
         content = self._content
         self._content = ''
         return content
@@ -83,11 +108,19 @@ class ExplorationFileSystem(object):
     In general, assets should be retrieved only within the context of the
     exploration that contains them, and should not be retrieved outside this
     context.
+
+    Args:
+        exploration_id: str. The id of the exploration.
     """
 
     _DEFAULT_VERSION_NUMBER = 1
 
     def __init__(self, exploration_id):
+        """Constructs a ExplorationFileSystem object.
+
+        Args:
+            exploration_id: str. The id of the exploration.
+        """
         self._exploration_id = exploration_id
 
     @property
@@ -98,6 +131,16 @@ def _get_file_metadata(self, filepath, version):
         """Return the desired file metadata.
 
         Returns None if the file does not exist.
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+            version: int. The version number of the file whose metadata is to be
+                returned.
+
+        Returns:
+            FileMetadataModel or None. The model instance representing the file
+                metadata with the given exploration_id, filepath, and version,
+                or None if the file does not exist.
         """
         if version is None:
             return file_models.FileMetadataModel.get_model(
@@ -110,6 +153,15 @@ def _get_file_data(self, filepath, version):
         """Return the desired file content.
 
         Returns None if the file does not exist.
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+            version: int. The version number of the file to be returned.
+
+        Returns:
+            FileModel or None. The model instance representing the file with the
+                given exploration_id, filepath, and version; or None if the file
+                does not exist.
         """
         if version is None:
             return file_models.FileModel.get_model(
@@ -119,7 +171,17 @@ def _get_file_data(self, filepath, version):
                 self._exploration_id, 'assets/%s' % filepath, version)
 
     def _save_file(self, user_id, filepath, raw_bytes):
-        """Create or update a file."""
+        """Create or update a file.
+
+        Args:
+            user_id: str. The user_id of the user who wants to create or update
+                a file.
+            filepath: str. The path to the relevant file within the exploration.
+            raw_bytes: str. The content to be stored in file.
+
+        Raises:
+            Exception: The maximum allowed file size is 1MB.
+        """
         if len(raw_bytes) > feconf.MAX_FILE_SIZE_BYTES:
             raise Exception('The maximum allowed file size is 1 MB.')
 
@@ -146,6 +208,16 @@ def get(self, filepath, version=None, mode=None):  # pylint: disable=unused-argu
 
         The 'mode' argument is unused. It is included so that this method
         signature matches that of other file systems.
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+            version: int or None. The version number of the file. None indicates
+                the latest version of the file.
+            mode: str. Unused argument.
+
+        Returns:
+            FileStreamWithMetadata or None. It returns FileStreamWithMetadata
+                domain object if the file exists. Otherwise, it returns None.
         """
         metadata = self._get_file_metadata(filepath, version)
         if metadata:
@@ -163,11 +235,25 @@ def get(self, filepath, version=None, mode=None):  # pylint: disable=unused-argu
             return None
 
     def commit(self, user_id, filepath, raw_bytes, unused_mimetype):
-        """Saves a raw bytestring as a file in the database."""
+        """Saves a raw bytestring as a file in the database.
+
+        Args:
+            user_id: str. The user_id of the user who wants to create or update
+                a file.
+            filepath: str. The path to the relevant file within the exploration.
+            raw_bytes: str. The content to be stored in the file.
+            unused_mimetype: str. Unused argument.
+        """
         self._save_file(user_id, filepath, raw_bytes)
 
     def delete(self, user_id, filepath):
-        """Marks the current version of a file as deleted."""
+        """Marks the current version of a file as deleted.
+
+        Args:
+            user_id: str. The user_id of the user who wants to create or update
+                a file.
+            filepath: str. The path to the relevant file within the exploration.
+        """
 
         metadata = self._get_file_metadata(filepath, None)
         if metadata:
@@ -178,7 +264,14 @@ def delete(self, user_id, filepath):
             data.delete(user_id, '')
 
     def isfile(self, filepath):
-        """Checks the existence of a file."""
+        """Checks the existence of a file.
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+
+        Returns:
+            bool. Whether the file exists.
+        """
         metadata = self._get_file_metadata(filepath, None)
         return bool(metadata)
 
@@ -186,12 +279,12 @@ def listdir(self, dir_name):
         """Lists all files in a directory.
 
         Args:
-            dir_name: The directory whose files should be listed. This should
-                not start with '/' or end with '/'.
+            dir_name: str. The directory whose files should be listed. This
+                should not start with '/' or end with '/'.
 
         Returns:
-            List of str. This is a lexicographically-sorted list of filenames,
-            each of which is prefixed with dir_name.
+            list(str). A lexicographically-sorted list of filenames,
+                each of which is prefixed with dir_name.
         """
         # The trailing slash is necessary to prevent non-identical directory
         # names with the same prefix from matching, e.g. /abcd/123.png should
@@ -214,13 +307,17 @@ class DiskBackedFileSystem(object):
     """Implementation for a disk-backed file system.
 
     This implementation ignores versioning and is used only by tests.
+
+    Attributes:
+        root: str. the path to append to the oppia/ directory.
+        exploration_id: str. The id of the exploration.
     """
 
     def __init__(self, root):
         """Constructor for this class.
 
         Args:
-            root: the path to append to the oppia/ directory.
+            root: str. the path to append to the oppia/ directory.
         """
         self._root = os.path.join(os.getcwd(), root)
         self._exploration_id = 'test'
@@ -230,11 +327,29 @@ def exploration_id(self):
         return self._exploration_id
 
     def isfile(self, filepath):
-        """Checks if a file exists."""
+        """Checks if a file exists.
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+
+        Returns:
+            bool. Whether the file exists.
+        """
         return os.path.isfile(os.path.join(self._root, filepath))
 
     def get(self, filepath, version=None, mode='r'):  # pylint: disable=unused-argument
-        """Returns a bytestring with the file content, but no metadata."""
+        """Returns a bytestring with the file content, but no metadata.
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+            version: int or None. The version number of the file. None indicates
+                the latest version of the file.
+            mode: str. The mode with which to open the file.
+
+        Returns:
+            FileStreamWithMetadata. A FileStreamWithMetadata domain object that
+                contains only the content of the file, but no metadata.
+        """
         content = utils.get_file_contents(
             os.path.join(self._root, filepath), raw_bytes=True, mode=mode)
         return FileStreamWithMetadata(content, None, None)
@@ -253,9 +368,17 @@ class GcsFileSystem(object):
     """Wrapper for a file system based on GCS.
 
     This implementation ignores versioning.
+
+    Attributes:
+        exploration_id: str. The id of the exploration.
     """
 
     def __init__(self, exploration_id):
+        """Constructs a GcsFileSystem object.
+
+        Args:
+            exploration_id: str. The id of the exploration.
+        """
         self._exploration_id = exploration_id
 
     @property
@@ -269,6 +392,12 @@ def get(self, filepath, version=None, mode='r'):  # pylint: disable=unused-argum
         raise NotImplementedError
 
     def commit(self, unused_user_id, filepath, raw_bytes, mimetype):
+        """Args:
+            unused_user_id: str. Unused argument.
+            filepath: str. The path to the relevant file within the exploration.
+            raw_bytes: str. The content to be stored in the file.
+            mimetype: str. The content-type of the cloud file.
+        """
         bucket_name = app_identity_services.get_gcs_resource_bucket_name()
 
         # Upload to GCS bucket with filepath
@@ -292,6 +421,7 @@ class AbstractFileSystem(object):
     """Interface for a file system."""
 
     def __init__(self, impl):
+        """Constructs a AbstractFileSystem object."""
         self._impl = impl
 
     @property
@@ -299,7 +429,14 @@ def impl(self):
         return self._impl
 
     def _check_filepath(self, filepath):
-        """Raises an error if a filepath is invalid."""
+        """Raises an error if a filepath is invalid.
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+
+        Raises:
+            IOError: Invalid filepath.
+        """
         base_dir = utils.vfs_construct_path(
             '/', self.impl.exploration_id, 'assets')
         absolute_path = utils.vfs_construct_path(base_dir, filepath)
@@ -310,17 +447,48 @@ def _check_filepath(self, filepath):
             raise IOError('Invalid filepath: %s' % filepath)
 
     def isfile(self, filepath):
-        """Checks if a file exists. Similar to os.path.isfile(...)."""
+        """Checks if a file exists. Similar to os.path.isfile(...).
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+
+        Returns:
+            bool. Whether the file exists.
+        """
         self._check_filepath(filepath)
         return self._impl.isfile(filepath)
 
     def open(self, filepath, version=None, mode='r'):
-        """Returns a stream with the file content. Similar to open(...)."""
+        """Returns a stream with the file content. Similar to open(...).
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+            version: int or None. The version number of the file. None indicates
+                the latest version of the file.
+            mode: str. The mode with which to open the file.
+
+        Returns:
+            FileStreamWithMetadata. The file stream domain object.
+        """
         self._check_filepath(filepath)
         return self._impl.get(filepath, version=version, mode=mode)
 
     def get(self, filepath, version=None, mode='r'):
-        """Returns a bytestring with the file content, but no metadata."""
+        """Returns a bytestring with the file content, but no metadata.
+
+        Args:
+            filepath: str. The path to the relevant file within the exploration.
+            version: int or None. The version number of the file. None indicates
+                the latest version of the file.
+            mode: str. The mode with which to open the file.
+
+        Returns:
+            FileStreamWithMetadata. The file stream domain object.
+
+        Raises:
+            IOError: The given (or latest) version of this file stream does not
+                exist.
+        """
         file_stream = self.open(filepath, version=version, mode=mode)
         if file_stream is None:
             raise IOError(
@@ -329,17 +497,40 @@ def get(self, filepath, version=None, mode='r'):
         return file_stream.read()
 
     def commit(self, user_id, filepath, raw_bytes, mimetype=None):
-        """Replaces the contents of the file with the given bytestring."""
+        """Replaces the contents of the file with the given by test string.
+
+        Args:
+            user_id: str. The user_id of the user who wants to create or update
+                a file.
+            filepath: str. The path to the relevant file within the exploration.
+            raw_bytes: str. The content to be stored in the file.
+            mimetype: str. The content-type of the file.
+        """
         raw_bytes = str(raw_bytes)
         self._check_filepath(filepath)
         self._impl.commit(user_id, filepath, raw_bytes, mimetype)
 
     def delete(self, user_id, filepath):
-        """Deletes a file and the metadata associated with it."""
+        """Deletes a file and the metadata associated with it.
+
+        Args:
+            user_id: str. The user_id of the user who wants to create or update
+                a file.
+            filepath: str. The path to the relevant file within the exploration.
+        """
         self._check_filepath(filepath)
         self._impl.delete(user_id, filepath)
 
     def listdir(self, dir_name):
-        """Lists all the files in a directory. Similar to os.listdir(...)."""
+        """Lists all the files in a directory. Similar to os.listdir(...).
+
+        Args:
+            dir_name: str. The directory whose files should be listed. This
+                should not start with '/' or end with '/'.
+
+        Returns:
+            list(str). A lexicographically-sorted list of filenames,
+            each of which is prefixed with dir_name.
+        """
         self._check_filepath(dir_name)
         return self._impl.listdir(dir_name)


Test patch: