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!
Can I take this up?

Sure! Assigned you to core.tests.test_utils.
Hi @seanlip, I would like to work on `core.domain.subscription_services.py`. Please assign me. 
Done, thanks!
can I work on this?
@Hsankesara Are you still working on `core.tests.test_utils`?
Hi @vibhor98 -- I've reassigned it to you. I think no progress for 3 weeks is sufficient grounds to reassign an issue!
Can you assign me one? @seanlip 
@yashLadha, I've assigned you to `utils.py`. :)
Great @DubeySandeep 

Resolving patch:
diff --git a/utils.py b/utils.py
--- a/utils.py
+++ b/utils.py
@@ -55,12 +55,31 @@ class ExplorationConversionError(Exception):
 
 
 def create_enum(*sequential, **names):
+    """Creates a enumerated constant
+
+    Args:
+        sequential: *. Sequence List to generate the enumerations.
+        names: *. Names of the enumerration.
+
+    Returns:
+        dict. Dictionary containing the enumerated constants.
+    """
     enums = dict(zip(sequential, sequential), **names)
     return type('Enum', (), enums)
 
 
 def get_file_contents(filepath, raw_bytes=False, mode='r'):
-    """Gets the contents of a file, given a relative filepath from oppia/."""
+    """Gets the contents of a file, given a relative filepath from oppia/.
+
+    Args:
+        filepath: str. A full path to the file.
+        raw_bytes: bool. Flag for the raw_bytes output.
+        mode: str. File opening mode, default is in read mode.
+
+    Returns:
+        *. Either the raw_bytes stream if the flag is set or the
+            decoded stream in utf-8 format.
+    """
     with open(filepath, mode) as f:
         return f.read() if raw_bytes else f.read().decode('utf-8')
 
@@ -69,12 +88,12 @@ def get_exploration_components_from_dir(dir_path):
     """Gets the (yaml, assets) from the contents of an exploration data dir.
 
     Args:
-      dir_path: a full path to the exploration root directory.
+        dir_path: str. a full path to the exploration root directory.
 
     Returns:
-      a 2-tuple, the first element of which is a yaml string, and the second
-      element of which is a list of (filepath, content) 2-tuples. The filepath
-      does not include the assets/ prefix.
+        *. A 2-tuple, the first element of which is a yaml string, and the
+        second element of which is a list of (filepath, content) 2-tuples.
+        The filepath does not include the assets/ prefix.
 
     Raises:
       Exception: if the following condition doesn't hold: "There is exactly one
@@ -129,17 +148,17 @@ def get_exploration_components_from_zip(zip_file_contents):
     """Gets the (yaml, assets) from the contents of an exploration zip file.
 
     Args:
-      zip_file_contents: a string of raw bytes representing the contents of
-        a zip file that comprises the exploration.
+        zip_file_contents: a string of raw bytes representing the contents of
+            a zip file that comprises the exploration.
 
     Returns:
-      a 2-tuple, the first element of which is a yaml string, and the second
-      element of which is a list of (filepath, content) 2-tuples. The filepath
-      does not include the assets/ prefix.
+        a 2-tuple, the first element of which is a yaml string, and the second
+        element of which is a list of (filepath, content) 2-tuples. The filepath
+        does not include the assets/ prefix.
 
     Raises:
-      Exception: if the following condition doesn't hold: "There is exactly one
-        file not in assets/, and this file has a .yaml suffix".
+        Exception: if the following condition doesn't hold: "There is exactly
+            one file not in assets/, and this file has a .yaml suffix".
     """
     memfile = StringIO.StringIO()
     memfile.write(zip_file_contents)
@@ -170,7 +189,14 @@ def get_exploration_components_from_zip(zip_file_contents):
 
 
 def get_comma_sep_string_from_list(items):
-    """Turns a list of items into a comma-separated string."""
+    """Turns a list of items into a comma-separated string.
+
+    Args:
+        items: list. List of the items.
+
+    Returns:
+        str. String containing the items in the list separated by commas.
+    """
 
     if not items:
         return ''
@@ -182,18 +208,46 @@ def get_comma_sep_string_from_list(items):
 
 
 def to_ascii(input_string):
-    """Change unicode characters in a string to ascii if possible."""
+    """Change unicode characters in a string to ascii if possible.
+
+    Args:
+        input_string: str. String to convert.
+
+    Returns:
+        str. String containing the ascii representation of the input string.
+    """
     return unicodedata.normalize(
         'NFKD', unicode(input_string)).encode('ascii', 'ignore')
 
 
 def yaml_from_dict(dictionary, width=80):
-    """Gets the YAML representation of a dict."""
+    """Gets the YAML representation of a dict.
+
+    Args:
+        dictionary: dict. Dictionary for conversion into yaml.
+        width: int. Width for the yaml representation, default value
+            is set to be of 80.
+
+    Returns:
+        str. Converted yaml of the passed dictionary.
+    """
     return yaml.safe_dump(dictionary, default_flow_style=False, width=width)
 
 
 def dict_from_yaml(yaml_str):
-    """Gets the dict representation of a YAML string."""
+    """Gets the dict representation of a YAML string.
+
+    Args:
+        yaml_str: str. Yaml string for conversion into dict.
+
+    Returns:
+        dict. Parsed dict representation of the yaml string.
+
+    Raises:
+        InavlidInputException: If the yaml string sent as the
+            parameter is unable to get parsed, them this error gets
+            raised.
+    """
     try:
         retrieved_dict = yaml.safe_load(yaml_str)
         assert isinstance(retrieved_dict, dict)
@@ -203,7 +257,16 @@ def dict_from_yaml(yaml_str):
 
 
 def recursively_remove_key(obj, key_to_remove):
-    """Recursively removes keys from a list or dict."""
+    """Recursively removes keys from a list or dict.
+
+    Args:
+        obj: *. List or dict passed for which the keys has to
+            be removed.
+        key_to_remove: str. Key value that has to be removed.
+
+    Returns:
+        *. Dict or list with a particular key value removed.
+    """
     if isinstance(obj, list):
         for item in obj:
             recursively_remove_key(item, key_to_remove)
@@ -215,7 +278,15 @@ def recursively_remove_key(obj, key_to_remove):
 
 
 def get_random_int(upper_bound):
-    """Returns a random integer in [0, upper_bound)."""
+    """Returns a random integer in [0, upper_bound).
+
+    Args:
+        upper_bound: int. Upper limit for generation of random
+            integer.
+
+    Returns:
+        int. Randomly generated integer less than the upper_bound.
+    """
     assert upper_bound >= 0 and isinstance(upper_bound, int)
 
     generator = random.SystemRandom()
@@ -223,7 +294,14 @@ def get_random_int(upper_bound):
 
 
 def get_random_choice(alist):
-    """Gets a random element from a list."""
+    """Gets a random element from a list.
+
+    Args:
+       alist: list(*). Input to get a random choice.
+
+    Returns:
+       *. Random element choosen from the passed input list.
+    """
     assert isinstance(alist, list) and len(alist) > 0
 
     index = get_random_int(len(alist))
@@ -231,7 +309,17 @@ def get_random_choice(alist):
 
 
 def convert_png_binary_to_data_url(content):
-    """Converts a png image string (represented by 'content') to a data URL."""
+    """Converts a png image string (represented by 'content') to a data URL.
+
+    Args:
+        content: str. PNG binary file content.
+
+    Returns:
+        *. Data url created from the binary content of the PNG.
+
+    Raises:
+        Exception: If the given binary string is not of a PNG image.
+    """
     if imghdr.what(None, content) == 'png':
         return 'data:image/png;base64,%s' % urllib.quote(
             content.encode('base64'))
@@ -246,15 +334,46 @@ def convert_png_to_data_url(filepath):
 
 
 def camelcase_to_hyphenated(camelcase_str):
+    """Camelcase to hyhpenated conversion of the passed string.
+
+    Args:
+        camelcase_str: str. Camelcase string representation.
+
+    Returns:
+        str. Hypenated string representation of the camelcase string.
+    """
     intermediate_str = re.sub('(.)([A-Z][a-z]+)', r'\1-\2', camelcase_str)
     return re.sub('([a-z0-9])([A-Z])', r'\1-\2', intermediate_str).lower()
 
+
 def camelcase_to_snakecase(camelcase_str):
+    """Camelcase to snake case conversion of the passed string.
+
+    Args:
+        camelcase_str: str. Camelcase string representation.
+
+    Returns:
+        str. Snakecase representation of the passed camelcase string.
+    """
     intermediate_str = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camelcase_str)
     return re.sub('([a-z0-9])([A-Z])', r'\1_\2', intermediate_str).lower()
 
+
 def set_url_query_parameter(url, param_name, param_value):
-    """Set or replace a query parameter, and return the modified URL."""
+    """Set or replace a query parameter, and return the modified URL.
+
+    Args:
+        url: str. URL string which contains the query parameter.
+        param_name: str. Parameter name to be removed.
+        param_value: str. Set the parameter value, if it exists.
+
+    Returns:
+        str. Formated URL that has query parameter set or replaced.
+
+    Raises:
+        Exception: If the query parameter sent is not of string type,
+            them this exception is raised.
+    """
     if not isinstance(param_name, basestring):
         raise Exception(
             'URL query parameter name must be a string, received %s'
@@ -285,7 +404,20 @@ def iterencode(self, o, _one_shot=False):
 
 
 def convert_to_hash(input_string, max_length):
-    """Convert a string to a SHA1 hash."""
+    """Convert a string to a SHA1 hash.
+
+    Args:
+        input_string: str. Input string for conversion to hash.
+        max_length: int. Maximum Length of the generated hash.
+
+    Returns:
+        str. Hash Value generated from the input_String of the
+            specified length.
+
+    Raises:
+        Exception: If the input string is not the instance of the basestring,
+            them this exception is raised.
+    """
     if not isinstance(input_string, basestring):
         raise Exception(
             'Expected string, received %s of type %s' %
@@ -298,6 +430,14 @@ def convert_to_hash(input_string, max_length):
 
 
 def base64_from_int(value):
+    """Converts the number into base64 representation.
+
+    Args:
+        value: int. Integer value for conversion into base64.
+
+    Returns:
+        *. Returns the base64 representation of the number passed.
+    """
     return base64.b64encode(bytes([value]))
 
 
@@ -305,7 +445,10 @@ def get_time_in_millisecs(datetime_obj):
     """Returns time in milliseconds since the Epoch.
 
     Args:
-      datetime_obj: An object of type datetime.datetime.
+        datetime_obj: datetime. An object of type datetime.datetime.
+
+    Returns:
+        float. This returns the time in the millisecond since the Epoch.
     """
     seconds = time.mktime(datetime_obj.timetuple()) * 1000
     return seconds + datetime_obj.microsecond / 1000.0
@@ -332,10 +475,23 @@ def are_datetimes_close(later_datetime, earlier_datetime):
 
 
 def generate_random_string(length):
+    """Generates a random string of the specified length.
+
+    Args:
+        length: int. Length of the string to be generated.
+
+    Returns:
+        str. Random string of specified length.
+    """
     return base64.urlsafe_b64encode(os.urandom(length))
 
 
 def generate_new_session_id():
+    """Generates a new session id.
+
+    Returns:
+        str. Random string of length 24.
+    """
     return generate_random_string(24)
 
 
@@ -386,10 +542,10 @@ def require_valid_name(name, name_type, allow_empty=False):
     """Generic name validation.
 
     Args:
-      name: the name to validate.
-      name_type: a human-readable string, like 'the exploration title' or
-        'a state name'. This will be shown in error messages.
-      allow_empty: if True, empty strings are allowed.
+        name: str. The name to validate.
+        name_type: str. A human-readable string, like 'the exploration title' or
+            'a state name'. This will be shown in error messages.
+        allow_empty: bool. If True, empty strings are allowed.
     """
     if not isinstance(name, basestring):
         raise ValidationError('%s must be a string.' % name_type)
@@ -422,6 +578,12 @@ def require_valid_name(name, name_type, allow_empty=False):
 def capitalize_string(input_string):
     """Converts the first character of a string to its uppercase equivalent (if
     it's a letter), and returns the result.
+
+    Args:
+        input_string: str. String to process (to capitalize).
+
+    Returns:
+        str. Capitalizes the string.
     """
     # This guards against empty strings.
     if input_string:
@@ -431,6 +593,15 @@ def capitalize_string(input_string):
 
 
 def get_hex_color_for_category(category):
+    """Returns the category, it returns the color associated with the category,
+    if the category is present in the app constants else given a default color.
+
+    Args:
+        category: str. Category to get color.
+
+    Returns:
+        str. Color assigned to that category.
+    """
     return (
         constants.CATEGORIES_TO_COLORS[category]
         if category in constants.CATEGORIES_TO_COLORS
@@ -438,6 +609,15 @@ def get_hex_color_for_category(category):
 
 
 def get_thumbnail_icon_url_for_category(category):
+    """Returns the category, it returns the associated thumbnail icon, if the
+    category is present in the app constants else given a default thumbnail.
+
+    Args:
+        category: str. Category to get Thumbnail icon.
+
+    Returns:
+        str. Path to the Thumbnail Icon assigned to that category.
+    """
     icon_name = (
         category if category in constants.CATEGORIES_TO_COLORS
         else constants.DEFAULT_THUMBNAIL_ICON)
@@ -448,6 +628,9 @@ def get_thumbnail_icon_url_for_category(category):
 def _get_short_language_description(full_language_description):
     """Given one of the descriptions in constants.ALL_LANGUAGE_CODES, generates
     the corresponding short description.
+
+    Args:
+        full_language_description: str. Short description of the language.
     """
     if ' (' not in full_language_description:
         return full_language_description
@@ -457,6 +640,13 @@ def _get_short_language_description(full_language_description):
 
 
 def get_all_language_codes_and_names():
+    """It parses the list of language codes and their corresponding names,
+    defined in the app constants.
+
+    Returns:
+        list(dict(str, str)). List of dictionary containing language code and
+            name mapped to their corresponding value.
+    """
     return [{
         'code': lc['code'],
         'name': _get_short_language_description(lc['description']),
@@ -519,7 +709,7 @@ def get_hashable_value(value):
 
     Returns:
         hashed_value: *. A new object that will always have the same hash for
-        "equivalent" values.
+            "equivalent" values.
     """
     if isinstance(value, list):
         return tuple(get_hashable_value(e) for e in value)


Test patch: