<commit_before>"""
Views for muckrock project
"""
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.exceptions import FieldError
from django.core.paginator import Paginator, InvalidPage
from django.db.models import Sum, FieldDoesNotExist
from django.http import Http404
from django.shortcuts import render_to_response, get_object_or_404, redirect
from django.template import RequestContext
from django.utils.decorators import method_decorator
from django.views.generic.list import ListView

from muckrock.agency.models import Agency
from muckrock.foia.models import FOIARequest, FOIAFile
from muckrock.forms import MRFilterForm
from muckrock.jurisdiction.models import Jurisdiction
from muckrock.news.models import Article
from muckrock.project.models import Project

import re
from haystack.views import SearchView

class MRFilterableListView(ListView):
    """
    The main feature of MRFilterableListView is the ability to filter
    a set of request objects by a dynamic dictionary of filters and
    lookup conditions. MRFilterableListView should be used in conjunction
    with MRFilterForm, available in the `muckrock.forms` module.

    To see an example of a subclass of MRFilterableListView that adds new
    filter fields, look at `muckrock.foia.views.RequestList`.
    """

    title = ''
    template_name = 'lists/base_list.html'
    default_sort = 'title'
    default_order = 'asc'

    def get_filters(self):
        """
        Filters should be the same as the fields in MRFilterForm, or whichever
        subclass of MRFilterForm is being used in as this class's `filter_form`.
        Filters are an array of key-value pairs.
        Required pairs are the field name and the [lookup condition][a].

        [a]: https://docs.djangoproject.com/en/1.7/ref/models/querysets/#field-lookups
        """
        # pylint: disable=no-self-use
        return [
            {
                'field': 'user',
                'lookup': 'exact',
            },
            {
                'field': 'agency',
                'lookup': 'exact',
            },
            {
                'field': 'jurisdiction',
                'lookup': 'exact',
            },
            {
                'field': 'tags',
                'lookup': 'name__in',
            },
        ]

    def clean_filter_value(self, filter_key, filter_value):
        """Cleans filter inputs to their expected values if detected as incorrect"""
        # pylint:disable=no-self-use
        # pylint:disable=too-many-branches
        if not filter_value:
            return None

        # tags need to be parsed into an array before filtering
        if filter_key == 'tags':
            filter_value = filter_value.split(',')
        if filter_key == 'user':
            # if looking up by PK, then result will be empty
            # if looking up by username, then result will have length
            if len(re.findall(r'\D+', filter_value)) > 0:
                try:
                    filter_value = User.objects.get(username=filter_value).pk
                except User.DoesNotExist:
                    filter_value = None
                # username is unique so only one result should be returned by get
        if filter_key == 'agency':
            if len(re.findall(r'\D+', filter_value)) > 0:
                try:
                    filter_value = Agency.objects.get(slug=filter_value).pk
                except Agency.DoesNotExist:
                    filter_value = None
                except Agency.MultipleObjectsReturned:
                    filter_value = Agency.objects.filter(slug=filter_value)[0]
        if filter_key == 'jurisdiction':
            if len(re.findall(r'\D+', filter_value)) > 0:
                try:
                    filter_value = Jurisdiction.objects.get(slug=filter_value).pk
                except Jurisdiction.DoesNotExist:
                    filter_value = None
                except Jurisdiction.MultipleObjectsReturned:
                    filter_value = Jurisdiction.objects.filter(slug=filter_value)[0]

        return filter_value

    def get_filter_data(self):
        """Returns a list of filter values and a url query for the filter."""
        get = self.request.GET
        filter_initials = {}
        filter_url = ''
        for filter_by in self.get_filters():
            filter_key = filter_by['field']
            filter_value = get.get(filter_key, None)
            filter_value = self.clean_filter_value(filter_key, filter_value)
            if filter_value:
                if isinstance(filter_value, list):
                    try:
                        filter_value = ', '.join(filter_value)
                    except TypeError:
                        filter_value = str(filter_value)
                kwarg = {filter_key: filter_value}
                try:
                    filter_initials.update(kwarg)
                    filter_url += '&' + str(filter_key) + '=' + str(filter_value)
                except ValueError:
                    pass
        return {
            'filter_initials': filter_initials,
            'filter_url': filter_url
        }

    def filter_list(self, objects):
        """Filters a list of objects"""
        get = self.request.GET
        kwargs = {}
        for filter_by in self.get_filters():
            filter_key = filter_by['field']
            filter_lookup = filter_by['lookup']
            filter_value = get.get(filter_key, None)
            filter_value = self.clean_filter_value(filter_key, filter_value)
            if filter_value:
                kwargs.update({'{0}__{1}'.format(filter_key, filter_lookup): filter_value})
        # tag filtering could add duplicate items to results, so .distinct() is used
        try:
            objects = objects.filter(**kwargs).distinct()
        except FieldError:
            pass
        except ValueError:
            error_msg = "Sorry, there was a problem with your filters. Please try filtering again."
            messages.error(self.request, error_msg)
        return objects

    def sort_list(self, objects):
        """Sorts the list of objects"""
        sort = self.request.GET.get('sort', self.default_sort)
        order = self.request.GET.get('order', self.default_order)
        # We need to make sure the field to sort by actually exists.
        # If the field doesn't exist, revert to the default field.
        # Otherwise, Django will throw a hard-to-catch FieldError.
        # It's hard to catch because the error isn't raised until
        # the QuerySet is evaluated. <Insert poop emoji here>
        try:
            # pylint:disable=protected-access
            self.model._meta.get_field_by_name(sort)
            # pylint:enable=protected-access
        except FieldDoesNotExist:
            sort = self.default_sort
        if order != 'asc':
            sort = '-' + sort
        objects = objects.order_by(sort)
        return objects

    def get_context_data(self, **kwargs):
        """Gets basic context, including title, form, and url"""
        context = super(MRFilterableListView, self).get_context_data(**kwargs)
        filter_data = self.get_filter_data()
        context['title'] = self.title
        context['per_page'] = int(self.get_paginate_by(context['object_list']))
        try:
            context['filter_form'] = MRFilterForm(initial=filter_data['filter_initials'])
        except ValueError:
            context['filter_form'] = MRFilterForm()
        context['filter_url'] = filter_data['filter_url']
        context['active_sort'] = self.request.GET.get('sort', self.default_sort)
        context['active_order'] = self.request.GET.get('order', self.default_order)
        return context

    def get_queryset(self):
        objects = super(MRFilterableListView, self).get_queryset()
        objects = self.filter_list(objects)
        objects = self.sort_list(objects)
        return objects

    def get_paginate_by(self, queryset):
        """Paginates list by the return value"""
        try:
            per_page = int(self.request.GET.get('per_page'))
            return max(min(per_page, 100), 5)
        except (ValueError, TypeError):
            return 25

class MRSearchView(SearchView):
    """Always lower case queries for case insensitive searches"""

    def get_query(self):
        """Lower case the query"""
        return super(MRSearchView, self).get_query().lower()

    def extra_context(self):
        """Adds per_page to context data"""
        context = super(MRSearchView, self).extra_context()
        context['per_page'] = int(self.request.GET.get('per_page', 25))
        return context

    def get_paginate_by(self):
        """Gets per_page the right way"""
        try:
            per_page = int(self.request.GET.get('per_page'))
            return max(min(per_page, 100), 5)
        except (ValueError, TypeError):
            return 25

    def build_page(self):
        """Circumvents the hard-coded haystack per page value."""
        # pylint: disable=pointless-statement
        # disabled pylint because this is not really my code
        # also, this should only be temporary (see issue #383)
        try:
            page_no = int(self.request.GET.get('page', 1))
        except (TypeError, ValueError):
            raise Http404("Not a valid number for page.")

        if page_no < 1:
            raise Http404("Pages should be 1 or greater.")

        start_offset = (page_no - 1) * self.results_per_page
        self.results[start_offset:start_offset + self.results_per_page]

        paginator = Paginator(self.results, self.get_paginate_by())
        try:
            page = paginator.page(page_no)
        except InvalidPage:
            raise Http404("No such page!")
        return (paginator, page)

def homepage(request):
    """Get all the details needed for the homepage"""
    # pylint: disable=unused-variable
    # pylint: disable=no-member
    try:
        articles = Article.objects.prefetch_related('projects')\
                                  .prefetch_related('authors')\
                                  .get_published()[:3]
        lead_article = articles[0]
        other_articles = articles[1:]
    except IndexError:
        # no published articles
        articles = None
        lead_article = None
        other_articles = None
    featured_projects = Project.objects.get_public().filter(featured=True)[:3]
    federal_government = Jurisdiction.objects.filter(level='f').first()
    public_requests = FOIARequest.objects.get_public()
    completed_requests = public_requests.filter(status__in=['done', 'partial'])\
                                        .order_by('date_done')[:6]
    stats = {
        'request_count': FOIARequest.objects.exclude(status='started').count(),
        'completed_count': FOIARequest.objects.filter(status__in=['done', 'partial']).count(),
        'page_count': FOIAFile.objects.aggregate(Sum('pages'))['pages__sum'],
        'agency_count': Agency.objects.get_approved().count()
    }
    return render_to_response('homepage.html', locals(),
                              context_instance=RequestContext(request))

def jurisdiction(request, jurisdiction=None, slug=None, idx=None, view=None):
    """Redirect to the jurisdiction page"""
    # pylint: disable=redefined-outer-name
    # pylint: disable=unused-argument

    if jurisdiction and idx:
        jmodel = get_object_or_404(Jurisdiction, slug=jurisdiction, pk=idx)
    if idx:
        jmodel = get_object_or_404(Jurisdiction, pk=idx)

    if not view:
        return redirect(jmodel)
    else:
        return redirect(jmodel.get_url(view))

def handler500(request):
    """
    500 error handler which includes request in the context.

    Templates: `500.html`
    Context: None
    """
    response = render_to_response('500.html', {}, context_instance=RequestContext(request))
    response.status_code = 500
    return response

# http://stackoverflow.com/a/8429311
def class_view_decorator(function_decorator):
    """Convert a function based decorator into a class based decorator usable
    on class based Views.

    Can't subclass the `view` as it breaks inheritance (super in particular),
    so we monkey-patch instead.
    """

    def simple_decorator(view):
        """Wrap the dispatch method"""
        view.dispatch = method_decorator(function_decorator)(view.dispatch)
        return view

    return simple_decorator
<commit_msg>actually displays recently completed requests<commit_after>"""
Views for muckrock project
"""
from django.contrib import messages
from django.contrib.auth.models import User
from django.core.exceptions import FieldError
from django.core.paginator import Paginator, InvalidPage
from django.db.models import Sum, FieldDoesNotExist
from django.http import Http404
from django.shortcuts import render_to_response, get_object_or_404, redirect
from django.template import RequestContext
from django.utils.decorators import method_decorator
from django.views.generic.list import ListView

from muckrock.agency.models import Agency
from muckrock.foia.models import FOIARequest, FOIAFile
from muckrock.forms import MRFilterForm
from muckrock.jurisdiction.models import Jurisdiction
from muckrock.news.models import Article
from muckrock.project.models import Project

import re
from haystack.views import SearchView

class MRFilterableListView(ListView):
    """
    The main feature of MRFilterableListView is the ability to filter
    a set of request objects by a dynamic dictionary of filters and
    lookup conditions. MRFilterableListView should be used in conjunction
    with MRFilterForm, available in the `muckrock.forms` module.

    To see an example of a subclass of MRFilterableListView that adds new
    filter fields, look at `muckrock.foia.views.RequestList`.
    """

    title = ''
    template_name = 'lists/base_list.html'
    default_sort = 'title'
    default_order = 'asc'

    def get_filters(self):
        """
        Filters should be the same as the fields in MRFilterForm, or whichever
        subclass of MRFilterForm is being used in as this class's `filter_form`.
        Filters are an array of key-value pairs.
        Required pairs are the field name and the [lookup condition][a].

        [a]: https://docs.djangoproject.com/en/1.7/ref/models/querysets/#field-lookups
        """
        # pylint: disable=no-self-use
        return [
            {
                'field': 'user',
                'lookup': 'exact',
            },
            {
                'field': 'agency',
                'lookup': 'exact',
            },
            {
                'field': 'jurisdiction',
                'lookup': 'exact',
            },
            {
                'field': 'tags',
                'lookup': 'name__in',
            },
        ]

    def clean_filter_value(self, filter_key, filter_value):
        """Cleans filter inputs to their expected values if detected as incorrect"""
        # pylint:disable=no-self-use
        # pylint:disable=too-many-branches
        if not filter_value:
            return None

        # tags need to be parsed into an array before filtering
        if filter_key == 'tags':
            filter_value = filter_value.split(',')
        if filter_key == 'user':
            # if looking up by PK, then result will be empty
            # if looking up by username, then result will have length
            if len(re.findall(r'\D+', filter_value)) > 0:
                try:
                    filter_value = User.objects.get(username=filter_value).pk
                except User.DoesNotExist:
                    filter_value = None
                # username is unique so only one result should be returned by get
        if filter_key == 'agency':
            if len(re.findall(r'\D+', filter_value)) > 0:
                try:
                    filter_value = Agency.objects.get(slug=filter_value).pk
                except Agency.DoesNotExist:
                    filter_value = None
                except Agency.MultipleObjectsReturned:
                    filter_value = Agency.objects.filter(slug=filter_value)[0]
        if filter_key == 'jurisdiction':
            if len(re.findall(r'\D+', filter_value)) > 0:
                try:
                    filter_value = Jurisdiction.objects.get(slug=filter_value).pk
                except Jurisdiction.DoesNotExist:
                    filter_value = None
                except Jurisdiction.MultipleObjectsReturned:
                    filter_value = Jurisdiction.objects.filter(slug=filter_value)[0]

        return filter_value

    def get_filter_data(self):
        """Returns a list of filter values and a url query for the filter."""
        get = self.request.GET
        filter_initials = {}
        filter_url = ''
        for filter_by in self.get_filters():
            filter_key = filter_by['field']
            filter_value = get.get(filter_key, None)
            filter_value = self.clean_filter_value(filter_key, filter_value)
            if filter_value:
                if isinstance(filter_value, list):
                    try:
                        filter_value = ', '.join(filter_value)
                    except TypeError:
                        filter_value = str(filter_value)
                kwarg = {filter_key: filter_value}
                try:
                    filter_initials.update(kwarg)
                    filter_url += '&' + str(filter_key) + '=' + str(filter_value)
                except ValueError:
                    pass
        return {
            'filter_initials': filter_initials,
            'filter_url': filter_url
        }

    def filter_list(self, objects):
        """Filters a list of objects"""
        get = self.request.GET
        kwargs = {}
        for filter_by in self.get_filters():
            filter_key = filter_by['field']
            filter_lookup = filter_by['lookup']
            filter_value = get.get(filter_key, None)
            filter_value = self.clean_filter_value(filter_key, filter_value)
            if filter_value:
                kwargs.update({'{0}__{1}'.format(filter_key, filter_lookup): filter_value})
        # tag filtering could add duplicate items to results, so .distinct() is used
        try:
            objects = objects.filter(**kwargs).distinct()
        except FieldError:
            pass
        except ValueError:
            error_msg = "Sorry, there was a problem with your filters. Please try filtering again."
            messages.error(self.request, error_msg)
        return objects

    def sort_list(self, objects):
        """Sorts the list of objects"""
        sort = self.request.GET.get('sort', self.default_sort)
        order = self.request.GET.get('order', self.default_order)
        # We need to make sure the field to sort by actually exists.
        # If the field doesn't exist, revert to the default field.
        # Otherwise, Django will throw a hard-to-catch FieldError.
        # It's hard to catch because the error isn't raised until
        # the QuerySet is evaluated. <Insert poop emoji here>
        try:
            # pylint:disable=protected-access
            self.model._meta.get_field_by_name(sort)
            # pylint:enable=protected-access
        except FieldDoesNotExist:
            sort = self.default_sort
        if order != 'asc':
            sort = '-' + sort
        objects = objects.order_by(sort)
        return objects

    def get_context_data(self, **kwargs):
        """Gets basic context, including title, form, and url"""
        context = super(MRFilterableListView, self).get_context_data(**kwargs)
        filter_data = self.get_filter_data()
        context['title'] = self.title
        context['per_page'] = int(self.get_paginate_by(context['object_list']))
        try:
            context['filter_form'] = MRFilterForm(initial=filter_data['filter_initials'])
        except ValueError:
            context['filter_form'] = MRFilterForm()
        context['filter_url'] = filter_data['filter_url']
        context['active_sort'] = self.request.GET.get('sort', self.default_sort)
        context['active_order'] = self.request.GET.get('order', self.default_order)
        return context

    def get_queryset(self):
        objects = super(MRFilterableListView, self).get_queryset()
        objects = self.filter_list(objects)
        objects = self.sort_list(objects)
        return objects

    def get_paginate_by(self, queryset):
        """Paginates list by the return value"""
        try:
            per_page = int(self.request.GET.get('per_page'))
            return max(min(per_page, 100), 5)
        except (ValueError, TypeError):
            return 25

class MRSearchView(SearchView):
    """Always lower case queries for case insensitive searches"""

    def get_query(self):
        """Lower case the query"""
        return super(MRSearchView, self).get_query().lower()

    def extra_context(self):
        """Adds per_page to context data"""
        context = super(MRSearchView, self).extra_context()
        context['per_page'] = int(self.request.GET.get('per_page', 25))
        return context

    def get_paginate_by(self):
        """Gets per_page the right way"""
        try:
            per_page = int(self.request.GET.get('per_page'))
            return max(min(per_page, 100), 5)
        except (ValueError, TypeError):
            return 25

    def build_page(self):
        """Circumvents the hard-coded haystack per page value."""
        # pylint: disable=pointless-statement
        # disabled pylint because this is not really my code
        # also, this should only be temporary (see issue #383)
        try:
            page_no = int(self.request.GET.get('page', 1))
        except (TypeError, ValueError):
            raise Http404("Not a valid number for page.")

        if page_no < 1:
            raise Http404("Pages should be 1 or greater.")

        start_offset = (page_no - 1) * self.results_per_page
        self.results[start_offset:start_offset + self.results_per_page]

        paginator = Paginator(self.results, self.get_paginate_by())
        try:
            page = paginator.page(page_no)
        except InvalidPage:
            raise Http404("No such page!")
        return (paginator, page)

def homepage(request):
    """Get all the details needed for the homepage"""
    # pylint: disable=unused-variable
    # pylint: disable=no-member
    try:
        articles = Article.objects.prefetch_related('projects')\
                                  .prefetch_related('authors')\
                                  .get_published()[:3]
        lead_article = articles[0]
        other_articles = articles[1:]
    except IndexError:
        # no published articles
        articles = None
        lead_article = None
        other_articles = None
    featured_projects = Project.objects.get_public().filter(featured=True)[:3]
    federal_government = Jurisdiction.objects.filter(level='f').first()
    public_requests = FOIARequest.objects.get_public()
    completed_requests = public_requests.filter(status__in=['done', 'partial'])\
                                        .order_by('-date_done')[:6]
    stats = {
        'request_count': FOIARequest.objects.exclude(status='started').count(),
        'completed_count': FOIARequest.objects.filter(status__in=['done', 'partial']).count(),
        'page_count': FOIAFile.objects.aggregate(Sum('pages'))['pages__sum'],
        'agency_count': Agency.objects.get_approved().count()
    }
    return render_to_response('homepage.html', locals(),
                              context_instance=RequestContext(request))

def jurisdiction(request, jurisdiction=None, slug=None, idx=None, view=None):
    """Redirect to the jurisdiction page"""
    # pylint: disable=redefined-outer-name
    # pylint: disable=unused-argument

    if jurisdiction and idx:
        jmodel = get_object_or_404(Jurisdiction, slug=jurisdiction, pk=idx)
    if idx:
        jmodel = get_object_or_404(Jurisdiction, pk=idx)

    if not view:
        return redirect(jmodel)
    else:
        return redirect(jmodel.get_url(view))

def handler500(request):
    """
    500 error handler which includes request in the context.

    Templates: `500.html`
    Context: None
    """
    response = render_to_response('500.html', {}, context_instance=RequestContext(request))
    response.status_code = 500
    return response

# http://stackoverflow.com/a/8429311
def class_view_decorator(function_decorator):
    """Convert a function based decorator into a class based decorator usable
    on class based Views.

    Can't subclass the `view` as it breaks inheritance (super in particular),
    so we monkey-patch instead.
    """

    def simple_decorator(view):
        """Wrap the dispatch method"""
        view.dispatch = method_decorator(function_decorator)(view.dispatch)
        return view

    return simple_decorator
<|endoftext|>


<commit_before>from collections import defaultdict
import json
from pathlib import Path

import joblib
import numpy as np
import pandas as pd
import torch
from tqdm import tqdm

import vak.device
import vak.files
import vak.labeled_timebins as lbl_tb_funcs
from vak import config, io, models, transforms
from vak.datasets.vocal_dataset import VocalDataset

def compute_metrics(metrics, y_true, y_pred, y_true_labels, y_pred_labels):
    """helper function to compute metrics

    Parameters
    ----------
    metrics : dict
        where keys are metric names and values are callables that compute the metric
        given ground truth and prediction
    y_true : torch.Tensor
        vector of labeled time bins
    y_pred : torch.Tensor
        vector of labeled time bins
    y_true_labels : str
        sequence of segment labels
    y_pred_labels : str
        sequence of segment labels

    Returns
    -------
    metric_vals : defaultdict
    """
    metric_vals = {}

    for metric_name, metric_callable in metrics.items():
        if metric_name == 'acc':
            metric_vals[metric_name] = metric_callable(y_pred, y_true)
        elif metric_name == 'levenshtein':
            metric_vals[metric_name] = metric_callable(y_pred_labels, y_true_labels)
        elif metric_name == 'segment_error_rate':
            metric_vals[metric_name] = metric_callable(y_pred_labels, y_true_labels)

    return metric_vals

def metrics_df_from_toml_path(toml_path,
                              min_segment_dur,
                              device='cuda',
                              spect_key='s',
                              timebins_key='t'):
    """computes evaluation metrics on a dataset from a config.toml file

    computes the metrics without and with transforms used for prediction

    Parameters
    ----------
    toml_path
    min_segment_dur
    device
    spect_key
    timebins_key

    Returns
    -------
    df : pandas.Dataframe
    """
    toml_path = Path(toml_path)
    cfg = config.parse.from_toml(toml_path)

    model_config_map = config.models.map_from_path(toml_path, cfg.eval.models)

    spect_standardizer = joblib.load(cfg.eval.spect_scaler_path)

    # ---- make eval dataset that we'll use to compute metrics
    # each batch will give us dict with 'spect', 'annot' and 'spect_path'
    # we can use 'spect_path' to find prediction in pred_dict and then compare to target
    # dict also includes 'padding_mask' so we can "unpad" the prediction vectors
    item_transform = transforms.get_defaults('eval',
                                             spect_standardizer,
                                             window_size=cfg.dataloader.window_size,
                                             return_padding_mask=True,
                                             )

    with cfg.eval.labelmap_path.open('r') as f:
        labelmap = json.load(f)

    eval_dataset = VocalDataset.from_csv(csv_path=cfg.eval.csv_path,
                                         split='test',
                                         labelmap=labelmap,
                                         spect_key=spect_key,
                                         timebins_key=timebins_key,
                                         item_transform=item_transform,
                                         )

    eval_data = torch.utils.data.DataLoader(dataset=eval_dataset,
                                            shuffle=False,
                                            # batch size 1 because each spectrogram reshaped into a batch of windows
                                            batch_size=1,
                                            num_workers=cfg.eval.num_workers)

    df = pd.read_csv(cfg.eval.csv_path)  # load because we use below for spect_paths as well
    # get timebin dur to use when converting labeled timebins to labels, onsets and offsets
    timebin_dur = io.dataframe.validate_and_get_timebin_dur(df)

    # ---- make pred dataset to actually do predictions
    # will call model.predict() and get back dict with predictions and spect paths
    pred_transform = transforms.get_defaults('predict',
                                             spect_standardizer,
                                             window_size=cfg.dataloader.window_size,
                                             return_padding_mask=False,
                                             )

    # can't use classmethod because we use 'test' split instead of 'predict' with no annotation
    # so we instantiate directly
    df_test = df[df['split'] == 'test']
    pred_dataset = VocalDataset(csv_path=cfg.eval.csv_path,
                                spect_paths=df_test['spect_path'],
                                annots=None,
                                labelmap=labelmap,
                                spect_key=spect_key,
                                timebins_key=timebins_key,
                                item_transform=pred_transform,
                                )

    pred_data = torch.utils.data.DataLoader(dataset=pred_dataset,
                                            shuffle=False,
                                            batch_size=1,  # hard coding to make this work for now
                                            num_workers=cfg.eval.num_workers)

    input_shape = pred_dataset.shape
    # if dataset returns spectrogram reshaped into windows,
    # throw out the window dimension; just want to tell network (channels, height, width) shape
    if len(input_shape) == 4:
        input_shape = input_shape[1:]

    models_map = models.from_model_config_map(
        model_config_map,
        num_classes=len(labelmap),
        input_shape=input_shape
    )

    if device is None:
        device = vak.device.get_default_device()

    records = defaultdict(list)  # will be used with pandas.DataFrame.from_records to make output csv
    to_long_tensor = transforms.ToLongTensor()

    for model_name, model in models_map.items():
        model.load(cfg.eval.checkpoint_path)
        metrics = model.metrics  # metric name -> callable map we use below in loop

        pred_dict = model.predict(pred_data=pred_data,
                                  device=device)

        progress_bar = tqdm(eval_data)
        for ind, batch in enumerate(progress_bar):
            y_true, padding_mask, spect_path = batch['annot'], batch['padding_mask'], batch['spect_path']
            if isinstance(spect_path, list) and len(spect_path) == 1:
                spect_path = spect_path[0]  # __getitem__ returns 1-element list for paths (with batch size 1)
            t = vak.files.spect.load(spect_path)[timebins_key]
            records['spect_path'].append(spect_path)  # remove str from tuple
            y_true = y_true.to(device)
            y_true_np = np.squeeze(y_true.cpu().numpy())
            y_true_labels, _, _ = lbl_tb_funcs.lbl_tb2segments(y_true_np,
                                                               labelmap=labelmap,
                                                               t=t)
            y_true_labels = ''.join(y_true_labels.tolist())

            y_pred = pred_dict[spect_path]
            y_pred = torch.argmax(y_pred, dim=1)  # assumes class dimension is 1
            y_pred = torch.flatten(y_pred)
            y_pred = y_pred.unsqueeze(0)[padding_mask]
            y_pred_np = np.squeeze(y_pred.cpu().numpy())
            y_pred_labels, _, _ = lbl_tb_funcs.lbl_tb2segments(y_pred_np,
                                                               labelmap=labelmap,
                                                               t=t,
                                                               min_segment_dur=None,
                                                               majority_vote=False)
            y_pred_labels = ''.join(y_pred_labels.tolist())

            metric_vals_batch = compute_metrics(metrics, y_true, y_pred, y_true_labels, y_pred_labels)
            for metric_name, metric_val in metric_vals_batch.items():
                records[metric_name].append(metric_val)

            # --- apply majority vote and min segment dur transforms separately
            # need segment_inds_list for both transforms
            segment_inds_list = lbl_tb_funcs.lbl_tb_segment_inds_list(y_pred_np,
                                                                      unlabeled_label=labelmap['unlabeled'])

            # ---- majority vote transform
            y_pred_np_mv = lbl_tb_funcs.majority_vote_transform(y_pred_np, segment_inds_list)
            y_pred_mv = to_long_tensor(y_pred_np_mv).to(device)
            y_pred_mv_labels, _, _ = lbl_tb_funcs.lbl_tb2segments(y_pred_np_mv,
                                                                  labelmap=labelmap,
                                                                  t=t,
                                                                  min_segment_dur=None,
                                                                  majority_vote=False)
            y_pred_mv_labels = ''.join(y_pred_mv_labels.tolist())
            metric_vals_batch_mv = compute_metrics(metrics, y_true, y_pred_mv,
                                                   y_true_labels, y_pred_mv_labels)
            for metric_name, metric_val in metric_vals_batch_mv.items():
                records[f'{metric_name}_majority_vote'].append(metric_val)

            # ---- min segment dur transform
            y_pred_np_mindur, _ = lbl_tb_funcs.remove_short_segments(y_pred_np,
                                                                     segment_inds_list,
                                                                     timebin_dur=timebin_dur,
                                                                     min_segment_dur=min_segment_dur,
                                                                     unlabeled_label=labelmap['unlabeled'])
            y_pred_mindur = to_long_tensor(y_pred_np_mindur).to(device)
            y_pred_mindur_labels, _, _ = lbl_tb_funcs.lbl_tb2segments(y_pred_np_mindur,
                                                                      labelmap=labelmap,
                                                                      t=t,
                                                                      min_segment_dur=None,
                                                                      majority_vote=False)
            y_pred_mindur_labels = ''.join(y_pred_mindur_labels.tolist())
            metric_vals_batch_mindur = compute_metrics(metrics, y_true, y_pred_mindur,
                                                       y_true_labels, y_pred_mindur_labels)
            for metric_name, metric_val in metric_vals_batch_mindur.items():
                records[f'{metric_name}_min_segment_dur'].append(metric_val)

            # ---- and finally both transforms, in same order we apply for prediction
            y_pred_np_mindur_mv, segment_inds_list = lbl_tb_funcs.remove_short_segments(y_pred_np,
                                                                                segment_inds_list,
                                                                                timebin_dur=timebin_dur,
                                                                                min_segment_dur=min_segment_dur,
                                                                                unlabeled_label=labelmap[
                                                                                'unlabeled'])
            y_pred_np_mindur_mv = lbl_tb_funcs.majority_vote_transform(y_pred_np_mindur_mv,
                                                                     segment_inds_list)
            y_pred_mindur_mv = to_long_tensor(y_pred_np_mindur_mv).to(device)
            y_pred_mindur_mv_labels, _, _ = lbl_tb_funcs.lbl_tb2segments(y_pred_np_mindur_mv,
                                                                         labelmap=labelmap,
                                                                         t=t,
                                                                         min_segment_dur=None,
                                                                         majority_vote=False)
            y_pred_mindur_mv_labels = ''.join(y_pred_mindur_mv_labels.tolist())
            metric_vals_batch_mindur_mv = compute_metrics(metrics, y_true, y_pred_mindur_mv,
                                                          y_true_labels, y_pred_mindur_mv_labels)
            for metric_name, metric_val in metric_vals_batch_mindur_mv.items():
                records[f'{metric_name}_min_dur_maj_vote'].append(metric_val)

        df = pd.DataFrame.from_records(records)
        return df

CONFIG_ROOT = Path('src/configs/BFSongRepository')
BIRD_ID_MIN_SEGMENT_DUR_MAP = {
    'bl26lb16': 0.02,
    'gr41rd51': 0.02,
    'gy6or6': 0.01,
    'or60yw70': 0.02,
}

def main():
    for bird_id, min_segment_dur in BIRD_ID_MIN_SEGMENT_DUR_MAP.items():
        toml_root = CONFIG_ROOT.joinpath(bird_id)
        eval_toml_paths = sorted(toml_root.glob('**/*eval*toml'))

        all_dfs = []
        for eval_toml_path in eval_toml_paths:
            print(f'computing metrics from dataset in .toml file: {eval_toml_path.name}')
            toml_df = metrics_df_from_toml_path(eval_toml_path, min_segment_dur)
            date = eval_toml_path.parents[0].name  # directory name is date
            print(f'date for this .toml file: {date}')
            toml_df['date'] = date
            all_dfs.append(toml_df)

        output_df = pd.concat(all_dfs)
        output_df['bird_id'] = bird_id
        print(f"adding 'bird_id' to concatenated data frames: {bird_id}")
        csv_fname = f'{bird_id}.metrics.csv'
        csv_path = Path('results/BFSongRepository').joinpath(csv_fname)
        print(f'saving csv as: {csv_path}')
        output_df.to_csv(csv_path, index=False)

if __name__ == '__main__':
    main()
<commit_msg>update bengalese finch eval transforms script<commit_after>from collections import defaultdict
import json
from pathlib import Path

import joblib
import numpy as np
import pandas as pd
import torch
from tqdm import tqdm

import vak.device
import vak.files
from vak.labeled_timebins import lbl_tb2segments, majority_vote_transform, lbl_tb_segment_inds_list, \
    remove_short_segments
from vak import config, io, models, transforms
from vak.datasets.vocal_dataset import VocalDataset

def compute_metrics(metrics, y_true, y_pred, y_true_labels, y_pred_labels):
    """helper function to compute metrics

    Parameters
    ----------
    metrics : dict
        where keys are metric names and values are callables that compute the metric
        given ground truth and prediction
    y_true : torch.Tensor
        vector of labeled time bins
    y_pred : torch.Tensor
        vector of labeled time bins
    y_true_labels : str
        sequence of segment labels
    y_pred_labels : str
        sequence of segment labels

    Returns
    -------
    metric_vals : defaultdict
    """
    metric_vals = {}

    for metric_name, metric_callable in metrics.items():
        if metric_name == 'acc':
            metric_vals[metric_name] = metric_callable(y_pred, y_true)
        elif metric_name == 'levenshtein':
            metric_vals[metric_name] = metric_callable(y_pred_labels, y_true_labels)
        elif metric_name == 'segment_error_rate':
            metric_vals[metric_name] = metric_callable(y_pred_labels, y_true_labels)

    return metric_vals

def metrics_df_from_toml_path(toml_path,
                              min_segment_dur,
                              n_timebin_from_onoffset,
                              device='cuda',
                              spect_key='s',
                              timebins_key='t'):
    """computes evaluation metrics on a dataset from a config.toml file

    computes the metrics without and with transforms used for prediction

    Parameters
    ----------
    toml_path : str, pathlib.Path
        to config .toml file
    min_segment_dur : float
        minimum duration of a segment, in seconds.
    n_timebin_from_onoffset : int
        number of timebins from onset or offset in which
        to count
    device : str
        device on which ``torch`` should place network.
        Default is 'cuda'
    spect_key : str
        key used to access spectrogram in files of arrays.
        Default is 's'
    timebins_key : str
        key used to access vector of timebins in files of arrays.
        Default is 't'

    Returns
    -------
    df : pandas.Dataframe
    """
    toml_path = Path(toml_path)
    cfg = config.parse.from_toml(toml_path)

    model_config_map = config.models.map_from_path(toml_path, cfg.eval.models)

    spect_standardizer = joblib.load(cfg.eval.spect_scaler_path)

    # ---- make eval dataset that we'll use to compute metrics
    # each batch will give us dict with 'spect', 'annot' and 'spect_path'
    # we can use 'spect_path' to find prediction in pred_dict and then compare to target
    # dict also includes 'padding_mask' so we can "unpad" the prediction vectors
    item_transform = transforms.get_defaults('eval',
                                             spect_standardizer,
                                             window_size=cfg.dataloader.window_size,
                                             return_padding_mask=True,
                                             )

    with cfg.eval.labelmap_path.open('r') as f:
        labelmap = json.load(f)

    eval_dataset = VocalDataset.from_csv(csv_path=cfg.eval.csv_path,
                                         split='test',
                                         labelmap=labelmap,
                                         spect_key=spect_key,
                                         timebins_key=timebins_key,
                                         item_transform=item_transform,
                                         )

    eval_data = torch.utils.data.DataLoader(dataset=eval_dataset,
                                            shuffle=False,
                                            # batch size 1 because each spectrogram reshaped into a batch of windows
                                            batch_size=1,
                                            num_workers=cfg.eval.num_workers)

    df = pd.read_csv(cfg.eval.csv_path)  # load because we use below for spect_paths as well
    # get timebin dur to use when converting labeled timebins to labels, onsets and offsets
    timebin_dur = io.dataframe.validate_and_get_timebin_dur(df)

    # ---- make pred dataset to actually do predictions
    # will call model.predict() and get back dict with predictions and spect paths
    pred_transform = transforms.get_defaults('predict',
                                             spect_standardizer,
                                             window_size=cfg.dataloader.window_size,
                                             return_padding_mask=False,
                                             )

    # can't use classmethod because we use 'test' split instead of 'predict' with no annotation
    # so we instantiate directly
    df_test = df[df['split'] == 'test']
    pred_dataset = VocalDataset(csv_path=cfg.eval.csv_path,
                                spect_paths=df_test['spect_path'],
                                annots=None,
                                labelmap=labelmap,
                                spect_key=spect_key,
                                timebins_key=timebins_key,
                                item_transform=pred_transform,
                                )

    pred_data = torch.utils.data.DataLoader(dataset=pred_dataset,
                                            shuffle=False,
                                            batch_size=1,  # hard coding to make this work for now
                                            num_workers=cfg.eval.num_workers)

    input_shape = pred_dataset.shape
    # if dataset returns spectrogram reshaped into windows,
    # throw out the window dimension; just want to tell network (channels, height, width) shape
    if len(input_shape) == 4:
        input_shape = input_shape[1:]

    models_map = models.from_model_config_map(
        model_config_map,
        num_classes=len(labelmap),
        input_shape=input_shape
    )

    if device is None:
        device = vak.device.get_default_device()

    records = defaultdict(list)  # will be used with pandas.DataFrame.from_records to make output csv
    to_long_tensor = transforms.ToLongTensor()

    for model_name, model in models_map.items():
        model.load(cfg.eval.checkpoint_path)
        metrics = model.metrics  # metric name -> callable map we use below in loop

        pred_dict = model.predict(pred_data=pred_data,
                                  device=device)

        progress_bar = tqdm(eval_data)
        for ind, batch in enumerate(progress_bar):
            y_true, padding_mask, spect_path = batch['annot'], batch['padding_mask'], batch['spect_path']
            if isinstance(spect_path, list) and len(spect_path) == 1:
                spect_path = spect_path[0]  # __getitem__ returns 1-element list for paths (with batch size 1)
            t = vak.files.spect.load(spect_path)[timebins_key]
            records['spect_path'].append(spect_path)  # remove str from tuple
            y_true = y_true.to(device)
            y_true_np = np.squeeze(y_true.cpu().numpy())
            y_true_labels, onsets_s, offsets_s = lbl_tb2segments(y_true_np,
                                                                 labelmap=labelmap,
                                                                 t=t)
            y_true_labels = ''.join(y_true_labels.tolist())

            y_pred = pred_dict[spect_path]
            y_pred = torch.argmax(y_pred, dim=1)  # assumes class dimension is 1
            y_pred = torch.flatten(y_pred)
            y_pred = y_pred.unsqueeze(0)[padding_mask]
            y_pred_np = np.squeeze(y_pred.cpu().numpy())
            y_pred_labels, _, _ = lbl_tb2segments(y_pred_np,
                                                  labelmap=labelmap,
                                                  t=t,
                                                  min_segment_dur=None,
                                                  majority_vote=False)
            y_pred_labels = ''.join(y_pred_labels.tolist())

            metric_vals_batch = compute_metrics(metrics, y_true, y_pred, y_true_labels, y_pred_labels)
            for metric_name, metric_val in metric_vals_batch.items():
                records[metric_name].append(metric_val)

            # --- apply majority vote and min segment dur transforms separately
            # need segment_inds_list for both transforms
            segment_inds_list = lbl_tb_segment_inds_list(y_pred_np,
                                                         unlabeled_label=labelmap['unlabeled'])

            # ---- majority vote transform
            y_pred_np_mv = majority_vote_transform(y_pred_np, segment_inds_list)
            y_pred_mv = to_long_tensor(y_pred_np_mv).to(device)
            y_pred_mv_labels, _, _ = lbl_tb2segments(y_pred_np_mv,
                                                     labelmap=labelmap,
                                                     t=t,
                                                     min_segment_dur=None,
                                                     majority_vote=False)
            y_pred_mv_labels = ''.join(y_pred_mv_labels.tolist())
            metric_vals_batch_mv = compute_metrics(metrics, y_true, y_pred_mv,
                                                   y_true_labels, y_pred_mv_labels)
            for metric_name, metric_val in metric_vals_batch_mv.items():
                records[f'{metric_name}_majority_vote'].append(metric_val)

            # ---- min segment dur transform
            y_pred_np_mindur, _ = remove_short_segments(y_pred_np,
                                                        segment_inds_list,
                                                        timebin_dur=timebin_dur,
                                                        min_segment_dur=min_segment_dur,
                                                        unlabeled_label=labelmap['unlabeled'])
            y_pred_mindur = to_long_tensor(y_pred_np_mindur).to(device)
            y_pred_mindur_labels, _, _ = lbl_tb2segments(y_pred_np_mindur,
                                                         labelmap=labelmap,
                                                         t=t,
                                                         min_segment_dur=None,
                                                         majority_vote=False)
            y_pred_mindur_labels = ''.join(y_pred_mindur_labels.tolist())
            metric_vals_batch_mindur = compute_metrics(metrics, y_true, y_pred_mindur,
                                                       y_true_labels, y_pred_mindur_labels)
            for metric_name, metric_val in metric_vals_batch_mindur.items():
                records[f'{metric_name}_min_segment_dur'].append(metric_val)

            # ---- and finally both transforms, in same order we apply for prediction
            y_pred_np_mindur_mv, segment_inds_list = remove_short_segments(y_pred_np,
                                                                           segment_inds_list,
                                                                           timebin_dur=timebin_dur,
                                                                           min_segment_dur=min_segment_dur,
                                                                           unlabeled_label=labelmap[
                                                                               'unlabeled'])
            y_pred_np_mindur_mv = majority_vote_transform(y_pred_np_mindur_mv,
                                                                     segment_inds_list)
            y_pred_mindur_mv = to_long_tensor(y_pred_np_mindur_mv).to(device)
            y_pred_mindur_mv_labels, _, _ = lbl_tb2segments(y_pred_np_mindur_mv,
                                                            labelmap=labelmap,
                                                            t=t,
                                                            min_segment_dur=None,
                                                            majority_vote=False)
            y_pred_mindur_mv_labels = ''.join(y_pred_mindur_mv_labels.tolist())
            metric_vals_batch_mindur_mv = compute_metrics(metrics, y_true, y_pred_mindur_mv,
                                                          y_true_labels, y_pred_mindur_mv_labels)
            for metric_name, metric_val in metric_vals_batch_mindur_mv.items():
                records[f'{metric_name}_min_dur_maj_vote'].append(metric_val)

            # here we measure the number of times a frame error occurs involving 'unlabeled' timebins
            # within some fixed number of timebins from an onset or offset.
            # the idea being that if those specific frame errors account
            # for a large number of all frame errors,
            # then most frame errors are due to noisiness of segmentation
            for y_pred_np_vec, y_pred_np_vec_label in zip(
                    (y_pred_np, y_pred_np_mindur, y_pred_np_mv, y_pred_np_mindur_mv),
                    ('y_pred_np', 'y_pred_np_mindur', 'y_pred_np_mv', 'y_pred_np_mindur_mv')
            ):
                frame_err_vec = y_true_np != y_pred_np_vec
                n_frame_err = int(frame_err_vec.sum().item())
                records[f'{y_pred_np_vec_label}_n_frame_err'].append(n_frame_err)
                unlabeled_err = np.logical_and(
                    frame_err_vec,
                    np.logical_or(y_true_np == labelmap['unlabeled'], y_pred_np_vec == labelmap['unlabeled'])
                )
                records[f'{y_pred_np_vec_label}_n_unlabeled_frame_err'].append(
                    int(unlabeled_err.sum().item())
                )
                t_unlabeled_err = t[unlabeled_err]
                t_unlabeled_err_from_onset_offset = [
                    min(np.abs(np.concatenate((onsets_s, offsets_s)) - a_time))
                    for a_time in t_unlabeled_err
                ]
                counts, _ = np.histogram(t_unlabeled_err_from_onset_offset, bins=np.arange(0.0, 1.0, timebin_dur))
                n_unlabeled_err_within_n_timebin = counts[:n_timebin_from_onoffset].sum()
                records[
                    f'{y_pred_np_vec_label}_n_unlabeled_err_within_{n_timebin_from_onoffset}_timebin_onoffset'
                ].append(n_unlabeled_err_within_n_timebin)

        df = pd.DataFrame.from_records(records)
        return df

CONFIG_ROOT = Path('src/configs/BFSongRepository')
BIRD_ID_MIN_SEGMENT_DUR_MAP = {
    'bl26lb16': 0.02,
    'gr41rd51': 0.02,
    'gy6or6': 0.01,
    'or60yw70': 0.02,
}
# number of timebins from an onset or offset
# in which we count
N_TIMEBINS_FROM_ONOFFSET = 2

def main():
    for bird_id, min_segment_dur in BIRD_ID_MIN_SEGMENT_DUR_MAP.items():
        toml_root = CONFIG_ROOT.joinpath(bird_id)
        eval_toml_paths = sorted(toml_root.glob('**/*eval*toml'))

        all_dfs = []
        for eval_toml_path in eval_toml_paths:
            print(f'computing metrics from dataset in .toml file: {eval_toml_path.name}')
            toml_df = metrics_df_from_toml_path(eval_toml_path,
                                                min_segment_dur,
                                                n_timebin_from_onoffset=N_TIMEBINS_FROM_ONOFFSET)
            date = eval_toml_path.parents[0].name  # directory name is date
            print(f'date for this .toml file: {date}')
            toml_df['date'] = date
            all_dfs.append(toml_df)

        output_df = pd.concat(all_dfs)
        output_df['bird_id'] = bird_id
        print(f"adding 'bird_id' to concatenated data frames: {bird_id}")
        csv_fname = f'{bird_id}.metrics.csv'
        csv_path = Path('results/BFSongRepository').joinpath(csv_fname)
        print(f'saving csv as: {csv_path}')
        output_df.to_csv(csv_path, index=False)

if __name__ == '__main__':
    main()
<|endoftext|>


<commit_before>"""
Test cases for launch_control.json_utils module
"""

from unittest import TestCase

from launch_control.utils.json import (
        ClassRegistry,
        IComplexJSONType,
        IFundamentalJSONType,
        ISimpleJSONType,
        PlainOldData,
        PluggableJSONDecoder,
        PluggableJSONEncoder,
        json,
        )

class ClassRegistryTestCase(TestCase):

    def setUp(self):
        self.registry = ClassRegistry()

    def test_register(self):
        class C(IComplexJSONType):
            @classmethod
            def get_json_class_name(self):
                return "class_c"
        self.registry.register(C)
        self.assertTrue('class_c' in self.registry.registered_types)
        self.assertEqual(self.registry.registered_types['class_c'], C)

    def test_register_checks_base_class(self):
        class C(object):
            pass
        self.assertRaises(TypeError, self.registry.register, C)

class IFundamentalJSONTypeTestCase(TestCase):

    def test_unimplemented_methods(self):
        class C(IFundamentalJSONType):
            pass
        self.assertRaises(NotImplementedError, C().to_raw_json)
        self.assertRaises(NotImplementedError, C.from_json, None)

    def test_register_rejects_fundamental_types(self):
        self.assertRaises(TypeError, ClassRegistry.register, IFundamentalJSONType)

    def test_encoding(self):
        class C(IFundamentalJSONType):
            def __init__(self, raw_data):
                self.raw_data = raw_data
            def to_raw_json(self):
                return self.raw_data
        self.assertEqual(json.dumps(C("foo"), cls=PluggableJSONEncoder), "foo")
        self.assertEqual(json.dumps(C("15"), cls=PluggableJSONEncoder), "15")
        self.assertEqual(json.dumps(C("{"), cls=PluggableJSONEncoder), "{")
        self.assertEqual(json.dumps(C("#"), cls=PluggableJSONEncoder), "#")

class ISimpleJSONTypeTestCase(TestCase):

    def test_unimplemented_methods(self):
        class C(ISimpleJSONType):
            pass
        self.assertRaises(NotImplementedError, C().to_json)
        self.assertRaises(NotImplementedError, C.from_json, None)

    def test_register_rejects_simple_types(self):
        self.assertRaises(TypeError, ClassRegistry.register, ISimpleJSONType)

    def test_encoding_and_decoding(self):
        class Enum(ISimpleJSONType):
            VALUES = [] # need to subclass
            def __init__(self, value):
                self.value = value
            def to_json(self):
                return self.VALUES[self.value]
            @classmethod
            def from_json(cls, json_string):
                return cls(cls.VALUES.index(json_string))
        class Weekday(Enum):
            VALUES = ["Monday", "Tuesday", "Wednesday", "Thursday",
                    "Friday", "Saturday", "Sunday"]
        for i in range(len(Weekday.VALUES)):
            day = Weekday(i)
            json_text = json.dumps(day, cls=PluggableJSONEncoder)
            self.assertEqual(json_text, '"' + Weekday.VALUES[i] + '"')
            day_loaded = json.loads(json_text, cls=PluggableJSONDecoder,
                    type_expr=Weekday)
            self.assertEqual(day_loaded.value, day.value)

class IComplexJSONTypeTestCase(TestCase):

    def setUp(self):
        class C(IComplexJSONType):
            pass
        self.C = C

    def test_unimplemented_methods(self):
        self.assertRaises(NotImplementedError, self.C.get_json_class_name)
        self.assertRaises(NotImplementedError, self.C().to_json)
        self.assertRaises(NotImplementedError, self.C().from_json, '')
        self.assertRaises(NotImplementedError, self.C.get_json_attr_types)

class PluggableJSONEncoderTestCase(TestCase):

    def test_encoder_checks_base_class(self):
        class C(object):
            pass
        self.assertRaises(TypeError, json.dumps, C(),
                cls=PluggableJSONEncoder)

class PluggableJSONDecoderTetsCase(TestCase):

    def setUp(self):
        self.registry = ClassRegistry()

    def test_decoder_raises_TypeError_with_unregistered_class(self):
        self.assertRaises(TypeError, json.loads, '{"__class__": "C"}',
                cls=PluggableJSONDecoder, registry=self.registry)

class EncodingTestCase(TestCase):

    def setUp(self):
        class A(PlainOldData):
            def __init__(self, yy, xx):
                self.yy = yy
                self.xx = xx
        class B(PlainOldData):
            def __init__(self, qq):
                self.qq = qq
        self.obj = A(xx=A(xx="inner", yy="member"), yy=B("quax"))

    def test_nested(self):
        json_text = json.dumps(self.obj, cls=PluggableJSONEncoder,
                sort_keys=True)
        expected_json_text = '{"__class__": "A", "xx": {' \
                '"__class__": "A", "xx": "inner", "yy": "member"}, ' \
                '"yy": {"__class__": "B", "qq": "quax"}}'
        self.assertEqual(json_text, expected_json_text)

    def test_nested_custom_hints(self):
        json_text = json.dumps(self.obj, cls=PluggableJSONEncoder,
                sort_keys=True, class_hint='klass')
        expected_json_text = '{"klass": "A", "xx": {' \
                '"klass": "A", "xx": "inner", "yy": "member"}, ' \
                '"yy": {"klass": "B", "qq": "quax"}}'
        self.assertEqual(json_text, expected_json_text)

    def test_registry_is_used_to_find_proxy(self):
        class X(object):
            def __init__(self, a):
                self.a = a
        class XProxy(IComplexJSONType):
            def __init__(self, obj):
                self.obj = obj
            def to_json(self):
                return {'value': self.obj.a}
            @classmethod
            def get_json_class_name(cls):
                return 'X'
        registry = ClassRegistry()
        registry.register_proxy(X, XProxy)
        x = X('foo')
        self.assertRaises(TypeError, json.dumps,
                x, cls=PluggableJSONEncoder,
                registry=ClassRegistry())
        json_text = json.dumps(x, cls=PluggableJSONEncoder,
                registry=registry, sort_keys=True)
        expected_json_text = '{"__class__": "X", "value": "foo"}'
        self.assertEqual(json_text, expected_json_text)

class FundamentalTypeProxying(TestCase):

    def setUp(self):
        class X(object):
            def __init__(self, a):
                self.a = a
        class XProxy(IFundamentalJSONType):
            def __init__(self, obj):
                self.obj = obj
            def to_raw_json(self):
                return self.obj.a
        self.registry = ClassRegistry()
        self.registry.register_proxy(X, XProxy)
        self.x = X('5')

    def test_encoding_with_hints(self):
        json_text = json.dumps(self.x,
                cls=PluggableJSONEncoder,
                registry=self.registry)
        self.assertEqual(json_text, '5')

    def test_encoding_without_hints(self):
        json_text = json.dumps(self.x,
                cls=PluggableJSONEncoder,
                registry=self.registry,
                class_hint=None)
        self.assertEqual(json_text, '5')

class SimpleTypeProxying(TestCase):

    def setUp(self):
        class Integer(object):
            def __init__(self, value):
                self.value = value
        class IntegerProxy(ISimpleJSONType):
            """
            Proxy class encoding Integers as strings
            """
            def __init__(self, obj):
                self.obj = obj
            def to_json(self):
                return str(self.obj.value)
            @classmethod
            def from_json(cls, json_str):
                return Integer(int(json_str))
        class Foo(IComplexJSONType):
            def __init__(self, i):
                self.i = i
            def to_json(self):
                return {'int': self.i}
            @classmethod
            def from_json(self, json_doc):
                return Foo(json_doc['int'])
            @classmethod
            def get_json_class_name(cls):
                return 'Foo'
            @classmethod
            def get_json_attr_types(cls):
                return {'int': self.Integer}
        self.Integer = Integer
        self.Foo = Foo
        self.registry = ClassRegistry()
        self.registry.register(Foo)
        self.registry.register_proxy(Integer, IntegerProxy)

    def test_encoding_with_hints(self):
        json_text = json.dumps(
                self.Integer(5),
                cls=PluggableJSONEncoder,
                registry=self.registry)
        self.assertEqual(json_text, '"5"')

    def test_encoding_without_hints(self):
        json_text = json.dumps(
                self.Integer(6),
                cls=PluggableJSONEncoder,
                registry=self.registry,
                class_hint=None)
        self.assertEqual(json_text, '"6"')

    def test_decoding_with_type_expr(self):
        obj = json.loads(
                '"5"',
                cls=PluggableJSONDecoder,
                registry=self.registry,
                type_expr=self.Integer)
        self.assertTrue(isinstance(obj, self.Integer))
        self.assertEqual(obj.value, 5)

    def test_decoding_without_type_expr(self):
        obj = json.loads(
                '"5"',
                cls=PluggableJSONDecoder,
                registry=self.registry)
        # Nope, it's not going to work
        self.assertEqual(obj, "5")

    def test_decoding_without_type_expr_in_nested_object_with_hinting(self):
        obj = json.loads(
                '{"__class__": "Foo", "int": "5"}',
                cls=PluggableJSONDecoder,
                registry=self.registry)
        self.assertTrue(isinstance(obj, self.Foo))
        self.assertTrue(isinstance(obj.i, self.Integer))
        self.assertEqual(obj.i.value, 5)

    def test_decoding_with_type_expr_in_nested_object_without_hinting(self):
        obj = json.loads('{"int": "5"}',
                cls=PluggableJSONDecoder,
                registry=self.registry,
                type_expr=self.Foo)
        self.assertTrue(isinstance(obj, self.Foo))
        self.assertTrue(isinstance(obj.i, self.Integer))
        self.assertEqual(obj.i.value, 5)

<commit_msg>Don't use backslashes in to create long docstrings<commit_after>"""
Test cases for launch_control.json_utils module
"""

from unittest import TestCase

from launch_control.utils.json import (
        ClassRegistry,
        IComplexJSONType,
        IFundamentalJSONType,
        ISimpleJSONType,
        PlainOldData,
        PluggableJSONDecoder,
        PluggableJSONEncoder,
        json,
        )

class ClassRegistryTestCase(TestCase):

    def setUp(self):
        self.registry = ClassRegistry()

    def test_register(self):
        class C(IComplexJSONType):
            @classmethod
            def get_json_class_name(self):
                return "class_c"
        self.registry.register(C)
        self.assertTrue('class_c' in self.registry.registered_types)
        self.assertEqual(self.registry.registered_types['class_c'], C)

    def test_register_checks_base_class(self):
        class C(object):
            pass
        self.assertRaises(TypeError, self.registry.register, C)

class IFundamentalJSONTypeTestCase(TestCase):

    def test_unimplemented_methods(self):
        class C(IFundamentalJSONType):
            pass
        self.assertRaises(NotImplementedError, C().to_raw_json)
        self.assertRaises(NotImplementedError, C.from_json, None)

    def test_register_rejects_fundamental_types(self):
        self.assertRaises(TypeError, ClassRegistry.register, IFundamentalJSONType)

    def test_encoding(self):
        class C(IFundamentalJSONType):
            def __init__(self, raw_data):
                self.raw_data = raw_data
            def to_raw_json(self):
                return self.raw_data
        self.assertEqual(json.dumps(C("foo"), cls=PluggableJSONEncoder), "foo")
        self.assertEqual(json.dumps(C("15"), cls=PluggableJSONEncoder), "15")
        self.assertEqual(json.dumps(C("{"), cls=PluggableJSONEncoder), "{")
        self.assertEqual(json.dumps(C("#"), cls=PluggableJSONEncoder), "#")

class ISimpleJSONTypeTestCase(TestCase):

    def test_unimplemented_methods(self):
        class C(ISimpleJSONType):
            pass
        self.assertRaises(NotImplementedError, C().to_json)
        self.assertRaises(NotImplementedError, C.from_json, None)

    def test_register_rejects_simple_types(self):
        self.assertRaises(TypeError, ClassRegistry.register, ISimpleJSONType)

    def test_encoding_and_decoding(self):
        class Enum(ISimpleJSONType):
            VALUES = [] # need to subclass
            def __init__(self, value):
                self.value = value
            def to_json(self):
                return self.VALUES[self.value]
            @classmethod
            def from_json(cls, json_string):
                return cls(cls.VALUES.index(json_string))
        class Weekday(Enum):
            VALUES = ["Monday", "Tuesday", "Wednesday", "Thursday",
                    "Friday", "Saturday", "Sunday"]
        for i in range(len(Weekday.VALUES)):
            day = Weekday(i)
            json_text = json.dumps(day, cls=PluggableJSONEncoder)
            self.assertEqual(json_text, '"' + Weekday.VALUES[i] + '"')
            day_loaded = json.loads(json_text, cls=PluggableJSONDecoder,
                    type_expr=Weekday)
            self.assertEqual(day_loaded.value, day.value)

class IComplexJSONTypeTestCase(TestCase):

    def setUp(self):
        class C(IComplexJSONType):
            pass
        self.C = C

    def test_unimplemented_methods(self):
        self.assertRaises(NotImplementedError, self.C.get_json_class_name)
        self.assertRaises(NotImplementedError, self.C().to_json)
        self.assertRaises(NotImplementedError, self.C().from_json, '')
        self.assertRaises(NotImplementedError, self.C.get_json_attr_types)

class PluggableJSONEncoderTestCase(TestCase):

    def test_encoder_checks_base_class(self):
        class C(object):
            pass
        self.assertRaises(TypeError, json.dumps, C(),
                cls=PluggableJSONEncoder)

class PluggableJSONDecoderTetsCase(TestCase):

    def setUp(self):
        self.registry = ClassRegistry()

    def test_decoder_raises_TypeError_with_unregistered_class(self):
        self.assertRaises(TypeError, json.loads, '{"__class__": "C"}',
                cls=PluggableJSONDecoder, registry=self.registry)

class EncodingTestCase(TestCase):

    def setUp(self):
        class A(PlainOldData):
            def __init__(self, yy, xx):
                self.yy = yy
                self.xx = xx
        class B(PlainOldData):
            def __init__(self, qq):
                self.qq = qq
        self.obj = A(xx=A(xx="inner", yy="member"), yy=B("quax"))

    def test_nested(self):
        json_text = json.dumps(self.obj, cls=PluggableJSONEncoder,
                sort_keys=True)
        expected_json_text = ('{"__class__": "A", "xx": {'
                '"__class__": "A", "xx": "inner", "yy": "member"}, '
                '"yy": {"__class__": "B", "qq": "quax"}}')
        self.assertEqual(json_text, expected_json_text)

    def test_nested_custom_hints(self):
        json_text = json.dumps(self.obj, cls=PluggableJSONEncoder,
                sort_keys=True, class_hint='klass')
        expected_json_text = ('{"klass": "A", "xx": {'
                '"klass": "A", "xx": "inner", "yy": "member"}, '
                '"yy": {"klass": "B", "qq": "quax"}}')
        self.assertEqual(json_text, expected_json_text)

    def test_registry_is_used_to_find_proxy(self):
        class X(object):
            def __init__(self, a):
                self.a = a
        class XProxy(IComplexJSONType):
            def __init__(self, obj):
                self.obj = obj
            def to_json(self):
                return {'value': self.obj.a}
            @classmethod
            def get_json_class_name(cls):
                return 'X'
        registry = ClassRegistry()
        registry.register_proxy(X, XProxy)
        x = X('foo')
        self.assertRaises(TypeError, json.dumps,
                x, cls=PluggableJSONEncoder,
                registry=ClassRegistry())
        json_text = json.dumps(x, cls=PluggableJSONEncoder,
                registry=registry, sort_keys=True)
        expected_json_text = '{"__class__": "X", "value": "foo"}'
        self.assertEqual(json_text, expected_json_text)

class FundamentalTypeProxying(TestCase):

    def setUp(self):
        class X(object):
            def __init__(self, a):
                self.a = a
        class XProxy(IFundamentalJSONType):
            def __init__(self, obj):
                self.obj = obj
            def to_raw_json(self):
                return self.obj.a
        self.registry = ClassRegistry()
        self.registry.register_proxy(X, XProxy)
        self.x = X('5')

    def test_encoding_with_hints(self):
        json_text = json.dumps(self.x,
                cls=PluggableJSONEncoder,
                registry=self.registry)
        self.assertEqual(json_text, '5')

    def test_encoding_without_hints(self):
        json_text = json.dumps(self.x,
                cls=PluggableJSONEncoder,
                registry=self.registry,
                class_hint=None)
        self.assertEqual(json_text, '5')

class SimpleTypeProxying(TestCase):

    def setUp(self):
        class Integer(object):
            def __init__(self, value):
                self.value = value
        class IntegerProxy(ISimpleJSONType):
            """
            Proxy class encoding Integers as strings
            """
            def __init__(self, obj):
                self.obj = obj
            def to_json(self):
                return str(self.obj.value)
            @classmethod
            def from_json(cls, json_str):
                return Integer(int(json_str))
        class Foo(IComplexJSONType):
            def __init__(self, i):
                self.i = i
            def to_json(self):
                return {'int': self.i}
            @classmethod
            def from_json(self, json_doc):
                return Foo(json_doc['int'])
            @classmethod
            def get_json_class_name(cls):
                return 'Foo'
            @classmethod
            def get_json_attr_types(cls):
                return {'int': self.Integer}
        self.Integer = Integer
        self.Foo = Foo
        self.registry = ClassRegistry()
        self.registry.register(Foo)
        self.registry.register_proxy(Integer, IntegerProxy)

    def test_encoding_with_hints(self):
        json_text = json.dumps(
                self.Integer(5),
                cls=PluggableJSONEncoder,
                registry=self.registry)
        self.assertEqual(json_text, '"5"')

    def test_encoding_without_hints(self):
        json_text = json.dumps(
                self.Integer(6),
                cls=PluggableJSONEncoder,
                registry=self.registry,
                class_hint=None)
        self.assertEqual(json_text, '"6"')

    def test_decoding_with_type_expr(self):
        obj = json.loads(
                '"5"',
                cls=PluggableJSONDecoder,
                registry=self.registry,
                type_expr=self.Integer)
        self.assertTrue(isinstance(obj, self.Integer))
        self.assertEqual(obj.value, 5)

    def test_decoding_without_type_expr(self):
        obj = json.loads(
                '"5"',
                cls=PluggableJSONDecoder,
                registry=self.registry)
        # Nope, it's not going to work
        self.assertEqual(obj, "5")

    def test_decoding_without_type_expr_in_nested_object_with_hinting(self):
        obj = json.loads(
                '{"__class__": "Foo", "int": "5"}',
                cls=PluggableJSONDecoder,
                registry=self.registry)
        self.assertTrue(isinstance(obj, self.Foo))
        self.assertTrue(isinstance(obj.i, self.Integer))
        self.assertEqual(obj.i.value, 5)

    def test_decoding_with_type_expr_in_nested_object_without_hinting(self):
        obj = json.loads('{"int": "5"}',
                cls=PluggableJSONDecoder,
                registry=self.registry,
                type_expr=self.Foo)
        self.assertTrue(isinstance(obj, self.Foo))
        self.assertTrue(isinstance(obj.i, self.Integer))
        self.assertEqual(obj.i.value, 5)

<|endoftext|>