Use type hints instead of type declarations in docstrings

Some type declarations in docstrings were outdated. These are fixed
and further all declarations are replaced by type hints.

Google style guide promotes the use of type annotated code:
https://github.com/google/styleguide/blob/gh-pages/pyguide.md#221-type-annotated-code

Upcoming commits should include type hints to be able to make use of
static type checkers as for example mypy.

Issue: HIC-174
Change-Id: I00462d4c6d32ba39e0c7ae9a17aaea98dac9b973
diff --git a/crashreport_stats/management/commands/stats.py b/crashreport_stats/management/commands/stats.py
index c30c2e0..35133c5 100644
--- a/crashreport_stats/management/commands/stats.py
+++ b/crashreport_stats/management/commands/stats.py
@@ -4,10 +4,11 @@
 heartbeats, crashes, and versions sent from Hiccup clients.
 """
 import datetime
+from typing import Dict, List, Optional
 
 from django.core.management.base import BaseCommand
 from django.db import transaction
-from django.db.models import Count, F, Q
+from django.db.models import Count, F, Model, Q, QuerySet
 from django.db.models.functions import TruncDate
 import pytz
 
@@ -17,6 +18,8 @@
     StatsMetadata,
     Version,
     VersionDaily,
+    _VersionStats,
+    _DailyVersionStats,
 )
 from crashreports.models import Crashreport, HeartBeat
 
@@ -29,34 +32,35 @@
     """Filter reports matching a report counter requirements.
 
     Attributes:
-        model (django.db.model): The report model.
-        name (str): The human-readable report counter name.
-        field_name (str): The counter name as defined in the stats model where
-            it is a field.
+        model: The report model.
+        name: The human-readable report counter name.
+        field_name:
+            The counter name as defined in the stats model where it is a field.
 
     """
 
-    def __init__(self, model, name, field_name):
+    def __init__(self, model: Model, name: str, field_name: str) -> None:
         """Initialise the filter.
 
         Args:
-            model (django.db.model): The report model.
-            name (str): The human-readable report counter name.
-            field_name (str): The counter name as defined in the stats model
-                where it is a field.
+            model: The report model.
+            name: The human-readable report counter name.
+            field_name:
+                The counter name as defined in the stats model where it is a
+                field.
 
         """
         self.model = model
         self.name = name
         self.field_name = field_name
 
-    def filter(self, query_objects):
+    def filter(self, query_objects: QuerySet) -> QuerySet:
         """Filter the reports.
 
         Args:
-            query_objects (QuerySet): The reports to filter.
+            query_objects: The reports to filter.
         Returns:
-            QuerySet: The reports matching this report counter requirements.
+            The reports matching this report counter requirements.
 
         """
         # pylint: disable=no-self-use
@@ -78,38 +82,41 @@
     """The crashreports counter filter.
 
     Attributes:
-        include_boot_reasons (list(str)): The boot reasons assumed to
-            characterise this crashreport ("OR"ed).
-        exclude_boot_reasons (list(str)): The boot reasons assumed to *not*
-            characterise this crashreport ("AND"ed).
-        inclusive_filter (Q): The boot reasons filter for filtering reports
-            that should be included.
-        exclusive_filter (Q): The boot reasons filter for filtering reports
-            that should *not* be included.
+        include_boot_reasons:
+            The boot reasons assumed to characterise this crashreport ("OR"ed).
+        exclude_boot_reasons:
+            The boot reasons assumed to *not* characterise this crashreport (
+            "AND"ed).
+        inclusive_filter:
+            The boot reasons filter for filtering reports that should be
+            included.
+        exclusive_filter:
+            The boot reasons filter for filtering reports that should *not*
+            be included.
 
     """
 
     def __init__(
         self,
-        name,
-        field_name,
-        include_boot_reasons=None,
-        exclude_boot_reasons=None,
-    ):
+        name: str,
+        field_name: str,
+        include_boot_reasons: Optional[List[str]] = None,
+        exclude_boot_reasons: Optional[List[str]] = None,
+    ) -> None:
         """Initialise the filter.
 
         One or both of `include_boot_reasons` and `exclude_boot_reasons` must
         be specified.
 
         Args:
-            name (str): The human-readable report counter name.
-            field_name (str):
+            name: The human-readable report counter name.
+            field_name:
                 The counter name as defined in the stats model where it is a
                 field.
-            include_boot_reasons (list(str), optional):
+            include_boot_reasons:
                 The boot reasons assumed to characterise this crashreport
                 ("OR"ed).
-            exclude_boot_reasons (list(str), optional):
+            exclude_boot_reasons:
                 The boot reasons assumed to *not* characterise this
                 crashreport ("AND"ed).
         Raises:
@@ -137,14 +144,14 @@
         self.exclusive_filter = self._create_query_filter(exclude_boot_reasons)
 
     @staticmethod
-    def _create_query_filter(reasons):
+    def _create_query_filter(reasons: Optional[List[str]]) -> Q:
         """Combine boot reasons into one filter.
 
         Args:
-            reasons (list(str)): List of boot reasons to include in filter.
+            reasons: List of boot reasons to include in filter.
         Returns:
-            django.db.models.query_utils.Q: Query that matches either of
-                reasons as boot_reason if list is not empty, otherwise None.
+            Query that matches either of reasons as boot_reason if list is
+            not empty, otherwise None.
 
         """
         if not reasons:
@@ -155,13 +162,13 @@
             query = query | Q(boot_reason=reason)
         return query
 
-    def filter(self, query_objects):
+    def filter(self, query_objects: QuerySet) -> QuerySet:
         """Filter the reports according to the inclusive and exclusive fitlers.
 
         Args:
-            query_objects (QuerySet): The reports to filter.
+            query_objects: The reports to filter.
         Returns:
-            QuerySet: The reports matching this report counter requirements.
+            The reports matching this report counter requirements.
 
         """
         if self.inclusive_filter:
@@ -179,34 +186,42 @@
     counterparts (_DailyVersionStats).
     """
 
-    def __init__(self, stats_model, daily_stats_model, version_field_name):
+    def __init__(
+        self,
+        stats_model: _VersionStats,
+        daily_stats_model: _DailyVersionStats,
+        version_field_name: str,
+    ) -> None:
         """Initialise the engine.
 
         Args:
-            stats_model (_VersionStats): The _VersionStats model to update
-                stats for.
-            daily_stats_model (_DailyVersionStats): The _DailyVersionStats
-                model to update stats for.
-            version_field_name (str): The version field name as specified in
-                the stats models.
+            stats_model: The _VersionStats model to update stats for.
+            daily_stats_model: The _DailyVersionStats model to update stats for.
+            version_field_name:
+                The version field name as specified in the stats models.
 
         """
         self.stats_model = stats_model
         self.daily_stats_model = daily_stats_model
         self.version_field_name = version_field_name
 
-    def _valid_objects(self, query_objects):
+    def _valid_objects(self, query_objects: QuerySet) -> QuerySet:
         """Filter out invalid reports.
 
         Returns:
-            QuerySet: All the valid reports.
+            All the valid reports.
 
         """
         # pylint: disable=no-self-use
         # self is potentially used by subclasses.
         return query_objects
 
-    def _objects_within_period(self, query_objects, up_to, starting_from=None):
+    def _objects_within_period(
+        self,
+        query_objects: QuerySet,
+        up_to: datetime.datetime,
+        starting_from: Optional[datetime.datetime] = None,
+    ) -> QuerySet:
         """Retrieve the reports within a specific period of time.
 
         The objects are filtered considering a specific period of time to allow
@@ -215,12 +230,11 @@
         bound must be specified to avoid race conditions.
 
         Args:
-            query_objects (QuerySet): The reports to filter.
-            up_to (datetime): The maximum timestamp to consider (inclusive).
-            starting_from (datetime, optional): The minimum timestamp to
-                consider (exclusive).
+            query_objects: The reports to filter.
+            up_to: The maximum timestamp to consider (inclusive).
+            starting_from: The minimum timestamp to consider (exclusive).
         Returns:
-            QuerySet: The reports received within a specific period of time.
+            The reports received within a specific period of time.
 
         """
         # pylint: disable=no-self-use
@@ -231,13 +245,12 @@
 
         return query_objects
 
-    def _unique_objects_per_day(self, query_objects):
+    def _unique_objects_per_day(self, query_objects: QuerySet) -> QuerySet:
         """Count the unique reports per version per day.
 
         Args:
-            query_objects (QuerySet): The reports to count.
-        Returns:
-            QuerySet: The unique reports grouped per version per day.
+            query_objects: The reports to count.
+        Returns: The unique reports grouped per version per day.
 
         """
         return (
@@ -248,11 +261,10 @@
             .annotate(count=Count("date", distinct=True))
         )
 
-    def delete_stats(self):
+    def delete_stats(self) -> Dict[str, int]:
         """Delete the general and daily stats instances.
 
-        Returns:
-            dict(str, int): The count of deleted entries per model name.
+        Returns: The count of deleted entries per model name.
 
         """
         # Clear the general stats, the daily stats will be deleted by cascading
@@ -260,7 +272,12 @@
         _, count_per_model = self.stats_model.objects.all().delete()
         return count_per_model
 
-    def update_stats(self, report_counter, up_to, starting_from=None):
+    def update_stats(
+        self,
+        report_counter: _ReportCounterFilter,
+        up_to: datetime.datetime,
+        starting_from: Optional[datetime.datetime] = None,
+    ) -> Dict[Model, Dict[str, int]]:
         """Update the statistics of the general and daily stats entries.
 
         The algorithm works as follow:
@@ -275,15 +292,13 @@
             while the sum of them per version updates the general stats.
 
         Args:
-            report_counter (_ReportCounterEngine): The report counter to
-                update the stats with.
-            up_to (datetime): The maximum timestamp to consider (inclusive).
-            starting_from (datetime, optional): The minimum timestamp to
-                consider (exclusive).
+            report_counter: The report counter to update the stats with.
+            up_to: The maximum timestamp to consider (inclusive).
+            starting_from: The minimum timestamp to consider (exclusive).
         Returns:
-            dict(str, dict(str, int)): The number of added entries and the
-                number of updated entries bundled in a dict, respectively
-                hashed with the keys 'created' and 'updated', per model name.
+            The number of added entries and the number of updated entries
+            bundled in a dict, respectively hashed with the keys 'created'
+            and 'updated', per model name.
 
         """
         counts_per_model = {
diff --git a/crashreport_stats/models.py b/crashreport_stats/models.py
index e4bb683..461db80 100644
--- a/crashreport_stats/models.py
+++ b/crashreport_stats/models.py
@@ -1,71 +1,76 @@
 """The stats models."""
-from django.db import models
+from django.db.models import (
+    BooleanField,
+    CASCADE,
+    CharField,
+    DateField,
+    DateTimeField,
+    ForeignKey,
+    IntegerField,
+    Model,
+)
 
 
-class _VersionStats(models.Model):
+class _VersionStats(Model):
     """The base class for all-time stats of a version.
 
     Sub-classes should be created to gather stats about a versioned
     end-product such as a software build.
 
     Attributes:
-        is_official_release (models.BooleanField): If this version is an
-            official release. Defaults to False.
-        is_beta_release (models.BooleanField): If this version is a beta
-            release. Defaults to False.
-        first_seen_on (models.DateField): Day this version has been seen for
-            the first time as reported by devices (not by the server). Defaults
-            to the current date.
-        released_on (models.DateField): Day this version has been released on.
-            Defaults to the current date.
-        heartbeats (models.IntegerField): The total heartbeats counted for this
-            version.
-        prob_crashes (models.IntegerField): The total probable crash reports
-            counted for this version.
-        smpl (models.IntegerField): The total SMPL reports counted for this
-            version.
-        other (models.IntegerField): The total of other reports counted for
-            this version.
+        is_official_release:
+            If this version is an official release. Defaults to False.
+        is_beta_release: If this version is a beta release. Defaults to False.
+        first_seen_on:
+            Day this version has been seen for the first time as reported by
+            devices (not by the server). Defaults to the current date.
+        released_on:
+            Day this version has been released on. Defaults to the current date.
+        heartbeats: The total heartbeats counted for this version.
+        prob_crashes: The total probable crash reports counted for this version.
+        smpl: The total SMPL reports counted for this version.
+        other: The total of other reports counted for this version.
 
     """
 
-    is_official_release = models.BooleanField(default=False)
-    is_beta_release = models.BooleanField(default=False)
-    first_seen_on = models.DateField()
-    released_on = models.DateField()
-    heartbeats = models.IntegerField(default=0)
-    prob_crashes = models.IntegerField(default=0)
-    smpl = models.IntegerField(default=0)
-    other = models.IntegerField(default=0)
+    is_official_release = BooleanField(default=False)
+    is_beta_release = BooleanField(default=False)
+    first_seen_on = DateField()
+    released_on = DateField()
+    heartbeats = IntegerField(default=0)
+    prob_crashes = IntegerField(default=0)
+    smpl = IntegerField(default=0)
+    other = IntegerField(default=0)
 
     class Meta:
         abstract = True
 
 
-class _DailyVersionStats(models.Model):
+class _DailyVersionStats(Model):
     """The base class for daily stats of a version.
 
     Sub-classes MUST define the foreign key `version` pointing back to the
     `_VersionStats` implementation they are gathering stats for.
 
     Attributes:
-        date (models.DateField): Day considered for the stats.
-        heartbeats (models.IntegerField): The total heartbeats counted for this
-            version on the day `date`.
-        prob_crashes (models.IntegerField): The total probable crash reports
-            counted for this version on the day `date`.
-        smpl (models.IntegerField): The total SMPL reports counted for this
-            version on the day `date`.
-        other (models.IntegerField): The total of other reports counted for
-            this version on the day `date`.
+        date: Day considered for the stats.
+        heartbeats:
+            The total heartbeats counted for this version on the day `date`.
+        prob_crashes:
+            The total probable crash reports counted for this version on the
+            day `date`.
+        smpl: The total SMPL reports counted for this version on the day `date`.
+        other:
+            The total of other reports counted for this version on the day
+            `date`.
 
     """
 
-    date = models.DateField()
-    heartbeats = models.IntegerField(default=0)
-    prob_crashes = models.IntegerField(default=0)
-    smpl = models.IntegerField(default=0)
-    other = models.IntegerField(default=0)
+    date = DateField()
+    heartbeats = IntegerField(default=0)
+    prob_crashes = IntegerField(default=0)
+    smpl = IntegerField(default=0)
+    other = IntegerField(default=0)
 
     class Meta:
         abstract = True
@@ -75,12 +80,12 @@
     """The all-time stats of a software version.
 
     Attributes:
-        build_fingerprint (models.CharField): The software build fingerprint
-            uniquely identifying this version.
+        build_fingerprint:
+            The software build fingerprint uniquely identifying this version.
 
     """
 
-    build_fingerprint = models.CharField(max_length=200, unique=True)
+    build_fingerprint = CharField(max_length=200, unique=True)
 
     def __str__(self):  # noqa: D105
         return self.build_fingerprint
@@ -90,16 +95,13 @@
     """The daily stats of a software version.
 
     Attributes:
-        version (models.ForeignKey): The software version object (`Version`)
-            these daily stats are about.
+        version:
+            The software version object (`Version`) these daily stats are about.
 
     """
 
-    version = models.ForeignKey(
-        Version,
-        db_index=True,
-        related_name="daily_stats",
-        on_delete=models.CASCADE,
+    version = ForeignKey(
+        Version, db_index=True, related_name="daily_stats", on_delete=CASCADE
     )
 
 
@@ -107,12 +109,12 @@
     """The all-time stats of a radio version.
 
     Attributes:
-        radio_version (models.CharField): The radio version number uniquely
-            identifying this version.
+        radio_version:
+            The radio version number uniquely identifying this version.
 
     """
 
-    radio_version = models.CharField(max_length=200, unique=True)
+    radio_version = CharField(max_length=200, unique=True)
 
     def __str__(self):  # noqa: D105
         return self.radio_version
@@ -122,26 +124,26 @@
     """The daily stats of a radio version.
 
     Attributes:
-        version (models.ForeignKey): The radio version object (`RadioVersion`)
-            these daily stats are about.
+        version:
+            The radio version object (`RadioVersion`) these daily stats are
+            about.
 
     """
 
-    version = models.ForeignKey(
+    version = ForeignKey(
         RadioVersion,
         db_index=True,
         related_name="daily_stats",
-        on_delete=models.CASCADE,
+        on_delete=CASCADE,
     )
 
 
-class StatsMetadata(models.Model):
+class StatsMetadata(Model):
     """The stats metadata.
 
     Attributes:
-        updated_at (models.DateTimeField): The last time the stats were
-            updated.
+        updated_at: The last time the stats were updated.
 
     """
 
-    updated_at = models.DateTimeField()
+    updated_at = DateTimeField()
diff --git a/crashreports/response_descriptions.py b/crashreports/response_descriptions.py
index 714f513..f97ccdf 100644
--- a/crashreports/response_descriptions.py
+++ b/crashreports/response_descriptions.py
@@ -1,18 +1,20 @@
 """Response descriptions for HTTP responses."""
+from typing import Tuple, Type
+
+from rest_framework.exceptions import APIException
 
 
-def default_desc(exception):
+def default_desc(exception: Type[APIException]) -> Tuple[int, str]:
     """Get the default response description for an exception.
 
     Args:
-        exception (rest_framework.exceptions.APIException):
+        exception:
             A subclass of APIException for which the response description
             should be returned.
 
     Returns:
-        (int, str):
-            A tuple containing the matching status code and default description
-            for the exception.
+        A tuple containing the matching status code and default description
+        for the exception.
 
     """
     return exception.status_code, str(exception.default_detail)
diff --git a/crashreports/tests.py b/crashreports/tests.py
index 354b330..b70f400 100644
--- a/crashreports/tests.py
+++ b/crashreports/tests.py
@@ -1,6 +1,7 @@
 """Test the API for crashreports, devices, heartbeats and logfiles."""
 import os
 import tempfile
+from typing import Optional
 
 from django.contrib.auth.models import User
 from django.urls import reverse
@@ -101,14 +102,14 @@
         return Dummy._update_copy(Dummy.DEFAULT_DUMMY_HEARTBEAT_VALUES, kwargs)
 
     @staticmethod
-    def crashreport_data(report_type=None, **kwargs):
+    def crashreport_data(report_type: Optional[str] = None, **kwargs):
         """Return the data required to create a crashreport.
 
         Use the values passed as keyword arguments or default to the ones
         from `Dummy.DEFAULT_DUMMY_CRASHREPORTS_VALUES`.
 
         Args:
-            report_type (str, optional): A valid value from
+            report_type: A valid value from
                 `Dummy.CRASH_TYPE_TO_BOOT_REASON_MAP.keys()` that will
                 define the boot reason if not explicitly defined in the
                 keyword arguments already.