<commit_before>import datetime
from dateutil import rrule
from dateutil.relativedelta import relativedelta
from decimal import Decimal
from itertools import groupby
import time
import calendar

from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.template.defaultfilters import slugify
from django.db.models import Sum, get_model
from django.contrib.sites.models import Site
from django.utils.functional import lazy
from django.core.urlresolvers import reverse

try:
    from django.utils import timezone
except ImportError:
    from timepiece import timezone

reverse_lazy = lazy(reverse, str)

def slugify_uniquely(s, queryset=None, field='slug'):
    """
    Returns a slug based on 's' that is unique for all instances of the given
    field in the given queryset.

    If no string is given or the given string contains no slugify-able
    characters, default to the given field name + N where N is the number of
    default slugs already in the database.
    """
    new_slug = new_slug_base = slugify(s)
    if queryset:
        queryset = queryset.filter(**{'%s__startswith' % field: new_slug_base})
        similar_slugs = [value[0] for value in queryset.values_list(field)]
        i = 1
        while new_slug in similar_slugs:
            new_slug = "%s%d" % (new_slug_base, i)
            i += 1
    return new_slug

def render_with(template_name):
    """
    Renders the view wrapped by this decorator with the given template.  The
    view should return the context to be used in the template, or an
    HttpResponse.

    If the view returns an HttpResponseRedirect, the decorator will redirect
    to the given URL, or to request.REQUEST['next'] (if it exists).
    """
    def render_with_decorator(view_func):
        def wrapper(*args, **kwargs):
            request = args[0]
            response = view_func(*args, **kwargs)

            if isinstance(response, HttpResponse):
                if isinstance(response, HttpResponseRedirect) and \
                  'next' in request.REQUEST:
                    return HttpResponseRedirect(request.REQUEST['next'])
                else:
                    return response
            else:
                # assume response is a context dictionary
                context = response
                return render_to_response(
                    template_name,
                    context,
                    context_instance=RequestContext(request),
                )
        return wrapper
    return render_with_decorator

DEFAULT_TIME_FORMATS = [
    '%H:%M',        # 23:15         => 23:15:00
    '%H:%M:%S',     # 05:50:21      => 05:50:21
    '%I:%M:%S %p',  # 11:40:53 PM   => 23:40:53
    '%I:%M %p',     # 6:21 AM       => 06:21:00
    '%I %p',        # 1 pm          => 13:00:00
    '%I:%M:%S%p',   # 8:45:52pm     => 23:45:52
    '%I:%M%p',      # 12:03am       => 00:03:00
    '%I%p',         # 12pm          => 12:00:00
    '%H',           # 22            => 22:00:00
]

def parse_time(time_str, input_formats=None):
    """
    This function will take a string with some sort of representation of time
    in it.  The string will be parsed using a variety of formats until a match
    is found for the format given.  The return value is a datetime.time object.
    """
    formats = input_formats or DEFAULT_TIME_FORMATS

    # iterate over all formats until we find a match
    for format in formats:
        try:
            # attempt to parse the time with the current format
            value = time.strptime(time_str, format)
        except ValueError:
            continue
        else:
            # turn the time_struct into a datetime.time object
            return timezone.make_aware(datetime.time(*value[3:6]),
                timezone.get_current_timezone())

    # return None if there's no matching format
    return None

def get_total_time(seconds):
    """
    Returns the specified number of seconds in an easy-to-read HH:MM:SS format
    """
    hours = int(seconds / 3600)
    seconds %= 3600
    minutes = int(seconds / 60)
    seconds %= 60

    return u'%02i:%02i:%02i' % (hours, minutes, seconds)

def get_month_start(from_day=None):
    if not from_day:
        from_day = datetime.date.today()
    from_day = datetime.datetime.combine(from_day,
        datetime.time(tzinfo=timezone.get_current_timezone()))
    return from_day.replace(day=1)

def get_week_start(day=None):
    if not day:
        day = datetime.date.today()
    isoweekday = day.isoweekday()
    if isoweekday != 1:
        day = day - datetime.timedelta(days=isoweekday - 1)
    day = datetime.datetime.combine(day,
        datetime.time(tzinfo=timezone.get_current_timezone()))
    return day

def get_last_billable_day(day=None):
    if not day:
        day = datetime.date.today()
    day += relativedelta(months=1)
    return get_week_start(get_month_start(day)) - \
        datetime.timedelta(days=1)

def generate_dates(start=None, end=None, by='week'):
    try:
        if not timezone.is_aware(start):
            start = timezone.make_aware(start, timezone.get_current_timezone())
    except AttributeError:
        if start:
            start = datetime.datetime.combine(start,
                datetime.time(tzinfo=timezone.get_current_timezone()))
    try:
        if not timezone.is_aware(end):
            end = timezone.make_aware(end, timezone.get_current_timezone())
    except AttributeError:
        if end:
            end = datetime.datetime.combine(end,
                datetime.time(tzinfo=timezone.get_current_timezone()))
    if by == 'month':
        start = get_month_start(start)
        return rrule.rrule(rrule.MONTHLY, dtstart=start, until=end)
    if by == 'week':
        start = get_week_start(start)
        return rrule.rrule(rrule.WEEKLY, dtstart=start, until=end, byweekday=0)
    if by == 'day':
        return rrule.rrule(rrule.DAILY, dtstart=start, until=end)

def get_week_window(day):
    start = get_week_start(day)
    end = start + datetime.timedelta(weeks=1)
    weeks = generate_dates(end=end, start=start, by='week')
    return list(weeks)

def date_filter(func):
    def inner_decorator(request, *args, **kwargs):
        from timepiece import forms as timepiece_forms
        if 'to_date' in request.GET:
            form = timepiece_forms.DateForm(request.GET)
            if form.is_valid():
                from_date, to_date = form.save()
                status = form.cleaned_data.get('status')
                activity = form.cleaned_data.get('activity')
            else:
                raise Http404
        else:
            form = timepiece_forms.DateForm()
            today = datetime.date.today()
            from_date = today.replace(day=1)
            to_date = from_date + relativedelta(months=1)
            status = activity = None
        return func(request, form, from_date, to_date, status, activity,
            *args, **kwargs)
    return inner_decorator

def get_hours(entries):
    hours = {'total': 0}
    for entry in entries:
        hours['total'] += entry['hours']
        if entry['billable']:
            hours['billable'] = entry['hours']
        else:
            hours['non_billable'] = entry['hours']
    return hours

def daily_summary(day_entries):
    projects = {}
    all_day = {}
    for name, entries in groupby(day_entries,
                                           lambda x: x['project__name']):
        hours = get_hours(entries)
        projects[name] = hours
        for key in hours.keys():
            if key in all_day:
                all_day[key] += hours[key]
            else:
                all_day[key] = hours[key]

    return (all_day, projects)

def grouped_totals(entries):
    select = {"day": {"date": """DATE_TRUNC('day', end_time)"""},
              "week": {"date": """DATE_TRUNC('week', end_time)"""}}
    weekly = entries.extra(select=select["week"]).values('date', 'billable')
    weekly = weekly.annotate(hours=Sum('hours')).order_by('date')
    daily = entries.extra(select=select["day"]).values('date', 'project__name',
                                                       'billable')
    daily = daily.annotate(hours=Sum('hours')).order_by('date',
                                                        'project__name')
    weeks = {}
    for week, week_entries in groupby(weekly, lambda x: x['date']):
        try:
            if timezone.is_naive(week):
                week = timezone.make_aware(week,
                    timezone.get_current_timezone())
        except AttributeError:
            week = datetime.datetime.combine(week,
                timezone.get_current_timezone())
        weeks[week] = get_hours(week_entries)
    days = []
    last_week = None
    for day, day_entries in groupby(daily, lambda x: x['date']):
        week = get_week_start(day)
        if last_week and week > last_week:
            yield last_week, weeks.get(last_week, {}), days
            days = []
        days.append((day, daily_summary(day_entries)))
        last_week = week
    yield week, weeks.get(week, {}), days

def find_overtime(dates):
    """Given a list of weekly summaries, return the overtime for each week"""
    return sum([day - 40 for day in dates if day > 40])

def format_leave(leave):
    """
    Formats leave time to ({project name: hours}, total hours)
    """
    leave_hours = 0
    leave_desc = {}
    for entry in leave:
        proj = entry.get('project__name')
        proj_hours = entry.get('hours')
        leave_desc[proj] = leave_desc.get(proj, 0) + proj_hours
        leave_hours += proj_hours
    return leave_desc, leave_hours

def get_hour_summaries(hours):
    """
    Coerce totals dictionary or list into a list of ordered tuples with %'s
    """
    if hasattr(hours, 'get'):
        billable = hours.get('billable', 0)
        non_billable = hours.get('non_billable', 0)
        worked = hours.get('total', 0)
    else:
        billable, non_billable, worked = hours
    if worked > 0:
        return [
            (billable, round(billable / worked * 100, 2)),
            (non_billable, round(non_billable / worked * 100, 2)),
            worked,
        ]
    else:
        return [(0, 0), (0, 0), 0]

def user_date_totals(user_entries):
    """Yield a user's name and a dictionary of their hours"""
    date_dict = {}
    for date, date_entries in groupby(user_entries, lambda x: x['date']):
        if isinstance(date, datetime.datetime):
            date = date.date()
        d_entries = list(date_entries)
        name = ' '.join((d_entries[0]['user__first_name'],
                        d_entries[0]['user__last_name']))
        hours = get_hours(d_entries)
        date_dict[date] = hours
    return name, date_dict

def project_totals(entries, date_headers, hour_type, overtime=False,
                   total_column=False):
    """
    Yield hour totals grouped by user and date. Optionally including overtime.
    """
    totals = [0 for date in date_headers]
    rows = []
    for user, user_entries in groupby(entries, lambda x: x['user']):
        name, date_dict = user_date_totals(user_entries)
        dates = []
        for index, day in enumerate(date_headers):
            if isinstance(day, datetime.datetime):
                day = day.date()
            total = date_dict.get(day, {}).get(hour_type, 0)
            totals[index] += total
            dates.append(total)
        if total_column:
            dates.append(sum(dates))
        if overtime:
            dates.append(find_overtime(dates))
        dates = [date or '' for date in dates]
        rows.append((name, dates))
    if total_column:
        totals.append(sum(totals))
    totals = [total or '' for total in totals]
    yield (rows, totals)

def payroll_totals(month_work_entries, month_leave_entries):
    """Summarizes monthly work and leave totals, grouped by user.

    Returns (labels, rows).
        labels -> {'billable': [status_label], 'nonbillable': [status_label]}
        rows -> maps totals for user.
            name:
            billable, nonbillable: {
                hours: [hours_for_label],
                total: total hours for status,
                percentage: percentage of work_total (out of 100),
            }
            work_total: sum of billable.total and nonbillable.total
            leave: from format_leave
            grand_total: sum of work_total and leave_total
    """
    def _get_name(entries):
        """Helper for getting the associated user's first and last name."""
        fname = entries[0].get('user__first_name', '') if entries else ''
        lname = entries[0].get('user__last_name', '') if entries else ''
        name = '{0} {1}'.format(fname, lname).strip()
        return name

    def _get_index(status, label):
        """Returns the index where label hours should be recorded.

        If the label does not exist, then it is added to the labels listing.
        Each row in rows, and the totals row, is updated accordingly.
        """
        if label in labels[status]:
            return labels[status].index(label)
        # Update labels, rows, and totals to reflect the addition.
        labels[status].append(label)
        for row in rows:
            row[status]['hours'].append(Decimal())
        totals[status]['hours'].append(Decimal())
        return len(labels[status]) - 1

    def _construct_row(name):
        """Constructs an empty row."""
        row = {'name': name}
        for status in ('billable', 'nonbillable'):
            row[status] = {
                'hours': [Decimal()] * len(labels[status]),
                'total': Decimal(),
                'percentage': Decimal(),
            }
        row['work_total'] = Decimal()
        row['leave'] = {'hours': {}, 'total': Decimal()}
        row['grand_total'] = Decimal()
        return row

    def _summarize_work(row):
        """Adds work_total and percentages for each status in row."""
        row['work_total'] = (row['billable']['total'] +
                row['nonbillable']['total'])
        if row['work_total']:
            for status in ('billable', 'nonbillable'):
                percentage = row[status]['total'] / row['work_total'] * 100
                row[status]['percentage'] = percentage

    labels = {'billable': [], 'nonbillable': []}
    rows = []
    totals = _construct_row('Totals')
    for user, work_entries in groupby(month_work_entries, lambda e: e['user']):
        work_entries = list(work_entries)
        leave_entries = month_leave_entries.filter(user=user)
        name = _get_name(work_entries)
        row = _construct_row(name)
        rows.append(row)

        for entry in work_entries:
            status = 'billable' if entry['billable'] else 'nonbillable'
            label = entry['project__type__label']
            index = _get_index(status, label)
            hours = entry['hours']
            row[status]['hours'][index] += hours
            totals[status]['hours'][index] += hours
            row[status]['total'] += hours
            totals[status]['total'] += hours
        _summarize_work(row)

        row['leave']['hours'], row['leave']['total'] = (
                format_leave(leave_entries))
        for desc, hours in row['leave']['hours'].items():
            new_hours = totals['leave']['hours'].get(desc, 0) + hours
            totals['leave']['hours'][desc] = new_hours
        totals['leave']['total'] += row['leave']['total']

        row['grand_total'] = row['work_total'] + row['leave']['total']

    _summarize_work(totals)
    rows.append(totals)
    return labels, rows
<commit_msg>refs #323 - Don't add totals row if there are no entries<commit_after>import datetime
from dateutil import rrule
from dateutil.relativedelta import relativedelta
from decimal import Decimal
from itertools import groupby
import time
import calendar

from django.http import HttpResponse, HttpResponseRedirect, Http404
from django.shortcuts import render_to_response
from django.template import RequestContext
from django.template.defaultfilters import slugify
from django.db.models import Sum, get_model
from django.contrib.sites.models import Site
from django.utils.functional import lazy
from django.core.urlresolvers import reverse

try:
    from django.utils import timezone
except ImportError:
    from timepiece import timezone

reverse_lazy = lazy(reverse, str)

def slugify_uniquely(s, queryset=None, field='slug'):
    """
    Returns a slug based on 's' that is unique for all instances of the given
    field in the given queryset.

    If no string is given or the given string contains no slugify-able
    characters, default to the given field name + N where N is the number of
    default slugs already in the database.
    """
    new_slug = new_slug_base = slugify(s)
    if queryset:
        queryset = queryset.filter(**{'%s__startswith' % field: new_slug_base})
        similar_slugs = [value[0] for value in queryset.values_list(field)]
        i = 1
        while new_slug in similar_slugs:
            new_slug = "%s%d" % (new_slug_base, i)
            i += 1
    return new_slug

def render_with(template_name):
    """
    Renders the view wrapped by this decorator with the given template.  The
    view should return the context to be used in the template, or an
    HttpResponse.

    If the view returns an HttpResponseRedirect, the decorator will redirect
    to the given URL, or to request.REQUEST['next'] (if it exists).
    """
    def render_with_decorator(view_func):
        def wrapper(*args, **kwargs):
            request = args[0]
            response = view_func(*args, **kwargs)

            if isinstance(response, HttpResponse):
                if isinstance(response, HttpResponseRedirect) and \
                  'next' in request.REQUEST:
                    return HttpResponseRedirect(request.REQUEST['next'])
                else:
                    return response
            else:
                # assume response is a context dictionary
                context = response
                return render_to_response(
                    template_name,
                    context,
                    context_instance=RequestContext(request),
                )
        return wrapper
    return render_with_decorator

DEFAULT_TIME_FORMATS = [
    '%H:%M',        # 23:15         => 23:15:00
    '%H:%M:%S',     # 05:50:21      => 05:50:21
    '%I:%M:%S %p',  # 11:40:53 PM   => 23:40:53
    '%I:%M %p',     # 6:21 AM       => 06:21:00
    '%I %p',        # 1 pm          => 13:00:00
    '%I:%M:%S%p',   # 8:45:52pm     => 23:45:52
    '%I:%M%p',      # 12:03am       => 00:03:00
    '%I%p',         # 12pm          => 12:00:00
    '%H',           # 22            => 22:00:00
]

def parse_time(time_str, input_formats=None):
    """
    This function will take a string with some sort of representation of time
    in it.  The string will be parsed using a variety of formats until a match
    is found for the format given.  The return value is a datetime.time object.
    """
    formats = input_formats or DEFAULT_TIME_FORMATS

    # iterate over all formats until we find a match
    for format in formats:
        try:
            # attempt to parse the time with the current format
            value = time.strptime(time_str, format)
        except ValueError:
            continue
        else:
            # turn the time_struct into a datetime.time object
            return timezone.make_aware(datetime.time(*value[3:6]),
                timezone.get_current_timezone())

    # return None if there's no matching format
    return None

def get_total_time(seconds):
    """
    Returns the specified number of seconds in an easy-to-read HH:MM:SS format
    """
    hours = int(seconds / 3600)
    seconds %= 3600
    minutes = int(seconds / 60)
    seconds %= 60

    return u'%02i:%02i:%02i' % (hours, minutes, seconds)

def get_month_start(from_day=None):
    if not from_day:
        from_day = datetime.date.today()
    from_day = datetime.datetime.combine(from_day,
        datetime.time(tzinfo=timezone.get_current_timezone()))
    return from_day.replace(day=1)

def get_week_start(day=None):
    if not day:
        day = datetime.date.today()
    isoweekday = day.isoweekday()
    if isoweekday != 1:
        day = day - datetime.timedelta(days=isoweekday - 1)
    day = datetime.datetime.combine(day,
        datetime.time(tzinfo=timezone.get_current_timezone()))
    return day

def get_last_billable_day(day=None):
    if not day:
        day = datetime.date.today()
    day += relativedelta(months=1)
    return get_week_start(get_month_start(day)) - \
        datetime.timedelta(days=1)

def generate_dates(start=None, end=None, by='week'):
    try:
        if not timezone.is_aware(start):
            start = timezone.make_aware(start, timezone.get_current_timezone())
    except AttributeError:
        if start:
            start = datetime.datetime.combine(start,
                datetime.time(tzinfo=timezone.get_current_timezone()))
    try:
        if not timezone.is_aware(end):
            end = timezone.make_aware(end, timezone.get_current_timezone())
    except AttributeError:
        if end:
            end = datetime.datetime.combine(end,
                datetime.time(tzinfo=timezone.get_current_timezone()))
    if by == 'month':
        start = get_month_start(start)
        return rrule.rrule(rrule.MONTHLY, dtstart=start, until=end)
    if by == 'week':
        start = get_week_start(start)
        return rrule.rrule(rrule.WEEKLY, dtstart=start, until=end, byweekday=0)
    if by == 'day':
        return rrule.rrule(rrule.DAILY, dtstart=start, until=end)

def get_week_window(day):
    start = get_week_start(day)
    end = start + datetime.timedelta(weeks=1)
    weeks = generate_dates(end=end, start=start, by='week')
    return list(weeks)

def date_filter(func):
    def inner_decorator(request, *args, **kwargs):
        from timepiece import forms as timepiece_forms
        if 'to_date' in request.GET:
            form = timepiece_forms.DateForm(request.GET)
            if form.is_valid():
                from_date, to_date = form.save()
                status = form.cleaned_data.get('status')
                activity = form.cleaned_data.get('activity')
            else:
                raise Http404
        else:
            form = timepiece_forms.DateForm()
            today = datetime.date.today()
            from_date = today.replace(day=1)
            to_date = from_date + relativedelta(months=1)
            status = activity = None
        return func(request, form, from_date, to_date, status, activity,
            *args, **kwargs)
    return inner_decorator

def get_hours(entries):
    hours = {'total': 0}
    for entry in entries:
        hours['total'] += entry['hours']
        if entry['billable']:
            hours['billable'] = entry['hours']
        else:
            hours['non_billable'] = entry['hours']
    return hours

def daily_summary(day_entries):
    projects = {}
    all_day = {}
    for name, entries in groupby(day_entries,
                                           lambda x: x['project__name']):
        hours = get_hours(entries)
        projects[name] = hours
        for key in hours.keys():
            if key in all_day:
                all_day[key] += hours[key]
            else:
                all_day[key] = hours[key]

    return (all_day, projects)

def grouped_totals(entries):
    select = {"day": {"date": """DATE_TRUNC('day', end_time)"""},
              "week": {"date": """DATE_TRUNC('week', end_time)"""}}
    weekly = entries.extra(select=select["week"]).values('date', 'billable')
    weekly = weekly.annotate(hours=Sum('hours')).order_by('date')
    daily = entries.extra(select=select["day"]).values('date', 'project__name',
                                                       'billable')
    daily = daily.annotate(hours=Sum('hours')).order_by('date',
                                                        'project__name')
    weeks = {}
    for week, week_entries in groupby(weekly, lambda x: x['date']):
        try:
            if timezone.is_naive(week):
                week = timezone.make_aware(week,
                    timezone.get_current_timezone())
        except AttributeError:
            week = datetime.datetime.combine(week,
                timezone.get_current_timezone())
        weeks[week] = get_hours(week_entries)
    days = []
    last_week = None
    for day, day_entries in groupby(daily, lambda x: x['date']):
        week = get_week_start(day)
        if last_week and week > last_week:
            yield last_week, weeks.get(last_week, {}), days
            days = []
        days.append((day, daily_summary(day_entries)))
        last_week = week
    yield week, weeks.get(week, {}), days

def find_overtime(dates):
    """Given a list of weekly summaries, return the overtime for each week"""
    return sum([day - 40 for day in dates if day > 40])

def format_leave(leave):
    """
    Formats leave time to ({project name: hours}, total hours)
    """
    leave_hours = 0
    leave_desc = {}
    for entry in leave:
        proj = entry.get('project__name')
        proj_hours = entry.get('hours')
        leave_desc[proj] = leave_desc.get(proj, 0) + proj_hours
        leave_hours += proj_hours
    return leave_desc, leave_hours

def get_hour_summaries(hours):
    """
    Coerce totals dictionary or list into a list of ordered tuples with %'s
    """
    if hasattr(hours, 'get'):
        billable = hours.get('billable', 0)
        non_billable = hours.get('non_billable', 0)
        worked = hours.get('total', 0)
    else:
        billable, non_billable, worked = hours
    if worked > 0:
        return [
            (billable, round(billable / worked * 100, 2)),
            (non_billable, round(non_billable / worked * 100, 2)),
            worked,
        ]
    else:
        return [(0, 0), (0, 0), 0]

def user_date_totals(user_entries):
    """Yield a user's name and a dictionary of their hours"""
    date_dict = {}
    for date, date_entries in groupby(user_entries, lambda x: x['date']):
        if isinstance(date, datetime.datetime):
            date = date.date()
        d_entries = list(date_entries)
        name = ' '.join((d_entries[0]['user__first_name'],
                        d_entries[0]['user__last_name']))
        hours = get_hours(d_entries)
        date_dict[date] = hours
    return name, date_dict

def project_totals(entries, date_headers, hour_type, overtime=False,
                   total_column=False):
    """
    Yield hour totals grouped by user and date. Optionally including overtime.
    """
    totals = [0 for date in date_headers]
    rows = []
    for user, user_entries in groupby(entries, lambda x: x['user']):
        name, date_dict = user_date_totals(user_entries)
        dates = []
        for index, day in enumerate(date_headers):
            if isinstance(day, datetime.datetime):
                day = day.date()
            total = date_dict.get(day, {}).get(hour_type, 0)
            totals[index] += total
            dates.append(total)
        if total_column:
            dates.append(sum(dates))
        if overtime:
            dates.append(find_overtime(dates))
        dates = [date or '' for date in dates]
        rows.append((name, dates))
    if total_column:
        totals.append(sum(totals))
    totals = [total or '' for total in totals]
    yield (rows, totals)

def payroll_totals(month_work_entries, month_leave_entries):
    """Summarizes monthly work and leave totals, grouped by user.

    Returns (labels, rows).
        labels -> {'billable': [status_label], 'nonbillable': [status_label]}
        rows -> maps totals for user.
            name:
            billable, nonbillable: {
                hours: [hours_for_label],
                total: total hours for status,
                percentage: percentage of work_total (out of 100),
            }
            work_total: sum of billable.total and nonbillable.total
            leave: from format_leave
            grand_total: sum of work_total and leave_total
    """
    def _get_name(entries):
        """Helper for getting the associated user's first and last name."""
        fname = entries[0].get('user__first_name', '') if entries else ''
        lname = entries[0].get('user__last_name', '') if entries else ''
        name = '{0} {1}'.format(fname, lname).strip()
        return name

    def _get_index(status, label):
        """Returns the index where label hours should be recorded.

        If the label does not exist, then it is added to the labels listing.
        Each row in rows, and the totals row, is updated accordingly.
        """
        if label in labels[status]:
            return labels[status].index(label)
        # Update labels, rows, and totals to reflect the addition.
        labels[status].append(label)
        for row in rows:
            row[status]['hours'].append(Decimal())
        totals[status]['hours'].append(Decimal())
        return len(labels[status]) - 1

    def _construct_row(name):
        """Constructs an empty row."""
        row = {'name': name}
        for status in ('billable', 'nonbillable'):
            row[status] = {
                'hours': [Decimal()] * len(labels[status]),
                'total': Decimal(),
                'percentage': Decimal(),
            }
        row['work_total'] = Decimal()
        row['leave'] = {'hours': {}, 'total': Decimal()}
        row['grand_total'] = Decimal()
        return row

    def _summarize_work(row):
        """Adds work_total and percentages for each status in row."""
        row['work_total'] = (row['billable']['total'] +
                row['nonbillable']['total'])
        if row['work_total']:
            for status in ('billable', 'nonbillable'):
                percentage = row[status]['total'] / row['work_total'] * 100
                row[status]['percentage'] = percentage

    labels = {'billable': [], 'nonbillable': []}
    rows = []
    totals = _construct_row('Totals')
    for user, work_entries in groupby(month_work_entries, lambda e: e['user']):
        work_entries = list(work_entries)
        leave_entries = month_leave_entries.filter(user=user)
        name = _get_name(work_entries)
        row = _construct_row(name)
        rows.append(row)

        for entry in work_entries:
            status = 'billable' if entry['billable'] else 'nonbillable'
            label = entry['project__type__label']
            index = _get_index(status, label)
            hours = entry['hours']
            row[status]['hours'][index] += hours
            totals[status]['hours'][index] += hours
            row[status]['total'] += hours
            totals[status]['total'] += hours
        _summarize_work(row)

        row['leave']['hours'], row['leave']['total'] = (
                format_leave(leave_entries))
        for desc, hours in row['leave']['hours'].items():
            new_hours = totals['leave']['hours'].get(desc, 0) + hours
            totals['leave']['hours'][desc] = new_hours
        totals['leave']['total'] += row['leave']['total']

        row['grand_total'] = row['work_total'] + row['leave']['total']

    _summarize_work(totals)
    if totals['grand_total']:
        rows.append(totals)
    return labels, rows
<|endoftext|>


<commit_before>#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Tests for titlecase"""

from __future__ import print_function, unicode_literals

import os
import sys
import tempfile
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))

from titlecase import titlecase, set_small_word_list, create_wordlist_filter_from_file

# (executed by `test_input_output` below)
TEST_DATA = (
    (
        "",
        ""
    ),
    (
        "word/word",
        "Word/Word"
    ),
    (
        "a title and/or string",
        "A Title and/or String"
    ),
    (
        "dance with me/let’s face the music and dance",
        "Dance With Me/Let’s Face the Music and Dance"
    ),
    (
        "a-b end-to-end two-not-three/three-by-four/five-and",
        "A-B End-to-End Two-Not-Three/Three-by-Four/Five-And"
    ),
    (
        "34th 3rd 2nd",
        "34th 3rd 2nd"
    ),
    (
        "Q&A with steve jobs: 'that's what happens in technology'",
        "Q&A With Steve Jobs: 'That's What Happens in Technology'"
    ),
    (
        "What is AT&T's problem?",
        "What Is AT&T's Problem?"
    ),
    (
        "Apple deal with AT&T falls through",
        "Apple Deal With AT&T Falls Through"
    ),
    (
        "Words with all consonants like cnn are acronyms",
        "Words With All Consonants Like CNN Are Acronyms"
    ),
    (
        "this v that",
        "This v That"
    ),
    (
        "this v. that",
        "This v. That"
    ),
    (
        "this vs that",
        "This vs That"
    ),
    (
        "this vs. that",
        "This vs. That"
    ),
    (
        "The SEC's Apple probe: what you need to know",
        "The SEC's Apple Probe: What You Need to Know"
    ),
    (
        "'by the Way, small word at the start but within quotes.'",
        "'By the Way, Small Word at the Start but Within Quotes.'"
    ),
    (
        "Small word at end is nothing to be afraid of",
        "Small Word at End Is Nothing to Be Afraid Of"
    ),
    (
        "Starting Sub-Phrase With a Small Word: a Trick, Perhaps?",
        "Starting Sub-Phrase With a Small Word: A Trick, Perhaps?"
    ),
    (
        "Sub-Phrase With a Small Word in Quotes: 'a Trick, Perhaps?'",
        "Sub-Phrase With a Small Word in Quotes: 'A Trick, Perhaps?'"
    ),
    (
        'sub-phrase with a small word in quotes: "a trick, perhaps?"',
        'Sub-Phrase With a Small Word in Quotes: "A Trick, Perhaps?"'
    ),
    (
        "Starting a Hyphen Delimited Sub-Phrase With a Small Word - a Trick, Perhaps?",
        "Starting a Hyphen Delimited Sub-Phrase With a Small Word - A Trick, Perhaps?"
    ),
    (
        "Hyphen Delimited Sub-Phrase With a Small Word in Quotes - 'a Trick, Perhaps?'",
        "Hyphen Delimited Sub-Phrase With a Small Word in Quotes - 'A Trick, Perhaps?'"
    ),
    (
        'Hyphen Delimited sub-phrase with a small word in quotes - "a trick, perhaps?"',
        'Hyphen Delimited Sub-Phrase With a Small Word in Quotes - "A Trick, Perhaps?"'
    ),
    (
        "Snakes on a Plane - The TV Edit - The Famous Line",
        "Snakes on a Plane - The TV Edit - The Famous Line"
    ),
    (
        "Starting an Em Dash Delimited Sub-Phrase With a Small Word — a Trick, Perhaps?",
        "Starting an Em Dash Delimited Sub-Phrase With a Small Word — A Trick, Perhaps?"
    ),
    (
        "Em Dash Delimited Sub-Phrase With a Small Word in Quotes — 'a Trick, Perhaps?'",
        "Em Dash Delimited Sub-Phrase With a Small Word in Quotes — 'A Trick, Perhaps?'"
    ),
    (
        'Em Dash Delimited sub-phrase with a small word in quotes — "a trick, perhaps?"',
        'Em Dash Delimited Sub-Phrase With a Small Word in Quotes — "A Trick, Perhaps?"'
    ),
    (
        "Snakes on a Plane — The TV Edit — The Famous Line",
        "Snakes on a Plane — The TV Edit — The Famous Line"
    ),
    (
        "EPISODE 7 — THE FORCE AWAKENS",
        "Episode 7 — The Force Awakens"
    ),
    (
        "episode 7 – The force awakens",
        "Episode 7 – The Force Awakens"
    ),
    (
        "THE CASE OF X ≤ 7",
        "The Case of X ≤ 7"
    ),
    (
        "the case of X ≤ 7",
        "The Case of X ≤ 7"
    ),
    (
        '"Nothing to Be Afraid of?"',
        '"Nothing to Be Afraid Of?"'
    ),
    (
        '"Nothing to be Afraid Of?"',
        '"Nothing to Be Afraid Of?"'
    ),
    (
        'a thing',
        'A Thing'
    ),
    (
        "2lmc Spool: 'gruber on OmniFocus and vapo(u)rware'",
        "2lmc Spool: 'Gruber on OmniFocus and Vapo(u)rware'"
    ),
    (
        'this is just an example.com',
        'This Is Just an example.com'
    ),
    (
        'this is something listed on del.icio.us',
        'This Is Something Listed on del.icio.us'
    ),
    (
        'iTunes should be unmolested',
        'iTunes Should Be Unmolested'
    ),
    (
        'reading between the lines of steve jobs’s ‘thoughts on music’',
        'Reading Between the Lines of Steve Jobs’s ‘Thoughts on Music’'
    ),
    (
        'seriously, ‘repair permissions’ is voodoo',
        'Seriously, ‘Repair Permissions’ Is Voodoo'
    ),
    (
        'generalissimo francisco franco: still dead; kieren McCarthy: still a jackass',
        'Generalissimo Francisco Franco: Still Dead; Kieren McCarthy: Still a Jackass'
    ),
    (
        "O'Reilly should be untouched",
        "O'Reilly Should Be Untouched"
    ),
    (
        "my name is o'reilly",
        "My Name Is O'Reilly"
    ),
    (
        "WASHINGTON, D.C. SHOULD BE FIXED BUT MIGHT BE A PROBLEM",
        "Washington, D.C. Should Be Fixed but Might Be a Problem"
    ),
    (
        "THIS IS ALL CAPS AND SHOULD BE ADDRESSED",
        "This Is All Caps and Should Be Addressed"
    ),
    (
        "Mr McTavish went to MacDonalds",
        "Mr McTavish Went to MacDonalds"
    ),
    (
        "this shouldn't\nget mangled",
        "This Shouldn't\nGet Mangled"
    ),
    (
        "this is http://foo.com",
        "This Is http://foo.com"
    ),
    (
        "mac mc MAC MC machine",
        "Mac Mc MAC MC Machine",
    ),
    (
        "FOO BAR 5TH ST",
        "Foo Bar 5th St",
    ),
    (
        "foo bar 5th st",
        "Foo Bar 5th St",
    ),
    (
        "l'grange l'grange l'Grange l'Grange",
        "l'Grange l'Grange l'Grange l'Grange",
    ),
    (
        "L'grange L'grange L'Grange L'Grange",
        "l'Grange l'Grange l'Grange l'Grange",
    ),
    (
        "l'GranGe",
        "l'GranGe",
    ),
    (
        "o'grange O'grange o'Grange O'Grange",
        "O'Grange O'Grange O'Grange O'Grange",
    ),
    (
        "o'grange's O'grange's o'Grange's O'Grange's",
        "O'Grange's O'Grange's O'Grange's O'Grange's",
    ),
    (
        "O'GranGe",
        "O'GranGe",
    ),
    (
        "o'melveny/o'doyle o'Melveny/o'doyle O'melveny/o'doyle o'melveny/o'Doyle o'melveny/O'doyle",
        "O'Melveny/O'Doyle O'Melveny/O'Doyle O'Melveny/O'Doyle O'Melveny/O'Doyle O'Melveny/O'Doyle",
    ),
    # These 'Mc' cases aim to ensure more consistent/predictable behavior.
    # The examples here are somewhat contrived, and are subject to change
    # if there is a compelling argument for updating their behavior.
    # See https://github.com/ppannuto/python-titlecase/issues/64
    (
        "mccay-mcbut-mcdo mcdonalds/mcby",
        "McCay-McBut-McDo McDonalds/McBy"
    ),
    (
        "oblon, spivak, mcclelland, maier & neustadt",
        "Oblon, Spivak, McClelland, Maier & Neustadt",
    ),
    (
        "Mcoblon, spivak, mcclelland, mcmaier, & mcneustadt",
        "McOblon, Spivak, McClelland, McMaier, & McNeustadt",
    ),
    (
        "mcfoo-bar, MCFOO-BAR, McFoo-bar, McFoo-Bar, mcfoo-mcbar, foo-mcbar",
        "McFoo-Bar, McFoo-Bar, McFoo-Bar, McFoo-Bar, McFoo-McBar, Foo-McBar",
    ),
    (
        "'QUOTE' A GREAT",
        "'Quote' a Great",
    ),
    (
        "‘QUOTE’ A GREAT",
        "‘Quote’ a Great",
    ),
    (
        "“YOUNG AND RESTLESS”",
        "“Young and Restless”",
    ),
    (
        "EL NIÑO A ARRIVÉ HIER",
        "El Niño a Arrivé Hier",
    ),
    (
        "YEA NO",
        "Yea No",
    ),
    (
        "ÝÆ ÑØ",
        "Ýæ Ñø",
    ),
    (
        "yea no",
        "Yea No",
    ),
    (
        "ýæ ñø",
        "Ýæ Ñø",
    ),
    # https://github.com/ppannuto/python-titlecase/pull/67
    (
        "Mr mr Mrs Ms Mss , Mr. and Mrs. Person",
        "Mr Mr Mrs Ms MSS , Mr. and Mrs. Person",
    ),
)

def test_initials_regex():
    """Test - uppercase initials regex with A.B"""
    from titlecase import UC_INITIALS
    assert bool(UC_INITIALS.match('A.B')) is True

def test_initials_regex_2():
    """Test - uppercase initials regex with A.B."""
    from titlecase import UC_INITIALS
    assert bool(UC_INITIALS.match('A.B.')) is True

def test_initials_regex_3():
    """Test - uppercase initials regex with ABCD"""
    from titlecase import UC_INITIALS
    assert bool(UC_INITIALS.match('ABCD')) is False

def check_input_matches_expected_output(in_, out):
    """Function yielded by test generator"""
    try:
        assert titlecase(in_) == out
    except AssertionError:
        print("{0} != {1}".format(titlecase(in_), out))
        raise

def test_at_and_t():
    def at_n_t(word, **kwargs):
        if word.upper() == "AT&T":
            return word.upper()
    print(titlecase("at&t", callback=at_n_t))
    assert titlecase("at&t", callback=at_n_t) == "AT&T"

def test_input_output():
    """Generated tests"""
    for data in TEST_DATA:
        yield check_input_matches_expected_output, data[0], data[1]

def test_callback():
    def abbreviation(word, **kwargs):
        if word.upper() in ('TCP', 'UDP'):
            return word.upper()
    s = 'a simple tcp and udp wrapper'
    # Note: this library is able to guess that all-consonant words are acronyms, so TCP
    # works naturally, but others will require the custom list
    assert titlecase(s) == 'A Simple TCP and Udp Wrapper'
    assert titlecase(s, callback=abbreviation) == 'A Simple TCP and UDP Wrapper'
    assert titlecase(s.upper(), callback=abbreviation) == 'A Simple TCP and UDP Wrapper'
    assert titlecase(u'crème brûlée', callback=lambda x, **kw: x.upper()) == u'CRÈME BRÛLÉE'

def test_set_small_word_list():
    assert titlecase('playing the game "words with friends"') == 'Playing the Game "Words With Friends"'
    set_small_word_list('a|an|the|with')
    assert titlecase('playing the game "words with friends"') == 'Playing the Game "Words with Friends"'

def test_custom_abbreviations():
    with tempfile.NamedTemporaryFile(mode='w') as f:
        f.write('UDP\nPPPoE\n')
        f.flush()
        # This works without a wordlist, because it begins mixed case
        assert titlecase('sending UDP packets over PPPoE works great') == 'Sending UDP Packets Over PPPoE Works Great'
        # Without a wordlist, this will do the "wrong" thing for the context
        assert titlecase('SENDING UDP PACKETS OVER PPPOE WORKS GREAT') == 'Sending Udp Packets Over Pppoe Works Great'
        # A wordlist can provide custom acronyms
        assert titlecase('sending UDP packets over PPPoE works great', callback=create_wordlist_filter_from_file(f.name)) == 'Sending UDP Packets Over PPPoE Works Great'

if __name__ == "__main__":
    import nose
    nose.main()
<commit_msg>Update testcase<commit_after>#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Tests for titlecase"""

from __future__ import print_function, unicode_literals

import os
import sys
import tempfile
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '../'))

from titlecase import titlecase, set_small_word_list, create_wordlist_filter_from_file

# (executed by `test_input_output` below)
TEST_DATA = (
    (
        "",
        ""
    ),
    (
        "word/word",
        "Word/Word"
    ),
    (
        "a title and/or string",
        "A Title and/or String"
    ),
    (
        "dance with me/let’s face the music and dance",
        "Dance With Me/Let’s Face the Music and Dance"
    ),
    (
        "a-b end-to-end two-not-three/three-by-four/five-and",
        "A-B End-to-End Two-Not-Three/Three-by-Four/Five-And"
    ),
    (
        "34th 3rd 2nd",
        "34th 3rd 2nd"
    ),
    (
        "Q&A with steve jobs: 'that's what happens in technology'",
        "Q&A With Steve Jobs: 'That's What Happens in Technology'"
    ),
    (
        "What is AT&T's problem?",
        "What Is AT&T's Problem?"
    ),
    (
        "Apple deal with AT&T falls through",
        "Apple Deal With AT&T Falls Through"
    ),
    (
        "Words with all consonants like cnn are acronyms",
        "Words With All Consonants Like CNN Are Acronyms"
    ),
    (
        "this v that",
        "This v That"
    ),
    (
        "this v. that",
        "This v. That"
    ),
    (
        "this vs that",
        "This vs That"
    ),
    (
        "this vs. that",
        "This vs. That"
    ),
    (
        "The SEC's Apple probe: what you need to know",
        "The SEC's Apple Probe: What You Need to Know"
    ),
    (
        "'by the Way, small word at the start but within quotes.'",
        "'By the Way, Small Word at the Start but Within Quotes.'"
    ),
    (
        "Small word at end is nothing to be afraid of",
        "Small Word at End Is Nothing to Be Afraid Of"
    ),
    (
        "Starting Sub-Phrase With a Small Word: a Trick, Perhaps?",
        "Starting Sub-Phrase With a Small Word: A Trick, Perhaps?"
    ),
    (
        "Sub-Phrase With a Small Word in Quotes: 'a Trick, Perhaps?'",
        "Sub-Phrase With a Small Word in Quotes: 'A Trick, Perhaps?'"
    ),
    (
        'sub-phrase with a small word in quotes: "a trick, perhaps?"',
        'Sub-Phrase With a Small Word in Quotes: "A Trick, Perhaps?"'
    ),
    (
        "Starting a Hyphen Delimited Sub-Phrase With a Small Word - a Trick, Perhaps?",
        "Starting a Hyphen Delimited Sub-Phrase With a Small Word - A Trick, Perhaps?"
    ),
    (
        "Hyphen Delimited Sub-Phrase With a Small Word in Quotes - 'a Trick, Perhaps?'",
        "Hyphen Delimited Sub-Phrase With a Small Word in Quotes - 'A Trick, Perhaps?'"
    ),
    (
        'Hyphen Delimited sub-phrase with a small word in quotes - "a trick, perhaps?"',
        'Hyphen Delimited Sub-Phrase With a Small Word in Quotes - "A Trick, Perhaps?"'
    ),
    (
        "Snakes on a Plane - The TV Edit - The Famous Line",
        "Snakes on a Plane - The TV Edit - The Famous Line"
    ),
    (
        "Starting an Em Dash Delimited Sub-Phrase With a Small Word — a Trick, Perhaps?",
        "Starting an Em Dash Delimited Sub-Phrase With a Small Word — A Trick, Perhaps?"
    ),
    (
        "Em Dash Delimited Sub-Phrase With a Small Word in Quotes — 'a Trick, Perhaps?'",
        "Em Dash Delimited Sub-Phrase With a Small Word in Quotes — 'A Trick, Perhaps?'"
    ),
    (
        'Em Dash Delimited sub-phrase with a small word in quotes — "a trick, perhaps?"',
        'Em Dash Delimited Sub-Phrase With a Small Word in Quotes — "A Trick, Perhaps?"'
    ),
    (
        "Snakes on a Plane — The TV Edit — The Famous Line",
        "Snakes on a Plane — The TV Edit — The Famous Line"
    ),
    (
        "EPISODE 7 — THE FORCE AWAKENS",
        "Episode 7 — The Force Awakens"
    ),
    (
        "episode 7 – The force awakens",
        "Episode 7 – The Force Awakens"
    ),
    (
        "THE CASE OF X ≤ 7",
        "The Case of X ≤ 7"
    ),
    (
        "the case of X ≤ 7",
        "The Case of X ≤ 7"
    ),
    (
        '"Nothing to Be Afraid of?"',
        '"Nothing to Be Afraid Of?"'
    ),
    (
        '"Nothing to be Afraid Of?"',
        '"Nothing to Be Afraid Of?"'
    ),
    (
        'a thing',
        'A Thing'
    ),
    (
        "2lmc Spool: 'gruber on OmniFocus and vapo(u)rware'",
        "2lmc Spool: 'Gruber on OmniFocus and Vapo(u)rware'"
    ),
    (
        'this is just an example.com',
        'This Is Just an example.com'
    ),
    (
        'this is something listed on del.icio.us',
        'This Is Something Listed on del.icio.us'
    ),
    (
        'iTunes should be unmolested',
        'iTunes Should Be Unmolested'
    ),
    (
        'reading between the lines of steve jobs’s ‘thoughts on music’',
        'Reading Between the Lines of Steve Jobs’s ‘Thoughts on Music’'
    ),
    (
        'seriously, ‘repair permissions’ is voodoo',
        'Seriously, ‘Repair Permissions’ Is Voodoo'
    ),
    (
        'generalissimo francisco franco: still dead; kieren McCarthy: still a jackass',
        'Generalissimo Francisco Franco: Still Dead; Kieren McCarthy: Still a Jackass'
    ),
    (
        "O'Reilly should be untouched",
        "O'Reilly Should Be Untouched"
    ),
    (
        "my name is o'reilly",
        "My Name Is O'Reilly"
    ),
    (
        "WASHINGTON, D.C. SHOULD BE FIXED BUT MIGHT BE A PROBLEM",
        "Washington, D.C. Should Be Fixed but Might Be a Problem"
    ),
    (
        "THIS IS ALL CAPS AND SHOULD BE ADDRESSED",
        "This Is All Caps and Should Be Addressed"
    ),
    (
        "Mr McTavish went to MacDonalds",
        "Mr McTavish Went to MacDonalds"
    ),
    (
        "this shouldn't\nget mangled",
        "This Shouldn't\nGet Mangled"
    ),
    (
        "this is http://foo.com",
        "This Is http://foo.com"
    ),
    (
        "mac mc MAC MC machine",
        "Mac Mc MAC MC Machine",
    ),
    (
        "FOO BAR 5TH ST",
        "Foo Bar 5th St",
    ),
    (
        "foo bar 5th st",
        "Foo Bar 5th St",
    ),
    (
        "l'grange l'grange l'Grange l'Grange",
        "l'Grange l'Grange l'Grange l'Grange",
    ),
    (
        "L'grange L'grange L'Grange L'Grange",
        "l'Grange l'Grange l'Grange l'Grange",
    ),
    (
        "l'GranGe",
        "l'GranGe",
    ),
    (
        "o'grange O'grange o'Grange O'Grange",
        "O'Grange O'Grange O'Grange O'Grange",
    ),
    (
        "o'grange's O'grange's o'Grange's O'Grange's",
        "O'Grange's O'Grange's O'Grange's O'Grange's",
    ),
    (
        "O'GranGe",
        "O'GranGe",
    ),
    (
        "o'melveny/o'doyle o'Melveny/o'doyle O'melveny/o'doyle o'melveny/o'Doyle o'melveny/O'doyle",
        "O'Melveny/O'Doyle O'Melveny/O'Doyle O'Melveny/O'Doyle O'Melveny/O'Doyle O'Melveny/O'Doyle",
    ),
    # These 'Mc' cases aim to ensure more consistent/predictable behavior.
    # The examples here are somewhat contrived, and are subject to change
    # if there is a compelling argument for updating their behavior.
    # See https://github.com/ppannuto/python-titlecase/issues/64
    (
        "mccay-mcbut-mcdo mcdonalds/mcby",
        "McCay-McBut-McDo McDonalds/McBy"
    ),
    (
        "oblon, spivak, mcclelland, maier & neustadt",
        "Oblon, Spivak, McClelland, Maier & Neustadt",
    ),
    (
        "Mcoblon, spivak, mcclelland, mcmaier, & mcneustadt",
        "McOblon, Spivak, McClelland, McMaier, & McNeustadt",
    ),
    (
        "mcfoo-bar, MCFOO-BAR, McFoo-bar, McFoo-Bar, mcfoo-mcbar, foo-mcbar",
        "McFoo-Bar, McFoo-Bar, McFoo-Bar, McFoo-Bar, McFoo-McBar, Foo-McBar",
    ),
    (
        "'QUOTE' A GREAT",
        "'Quote' a Great",
    ),
    (
        "‘QUOTE’ A GREAT",
        "‘Quote’ a Great",
    ),
    (
        "“YOUNG AND RESTLESS”",
        "“Young and Restless”",
    ),
    (
        "EL NIÑO A ARRIVÉ HIER",
        "El Niño a Arrivé Hier",
    ),
    (
        "YEA NO",
        "Yea No",
    ),
    (
        "ÝÆ ÑØ",
        "Ýæ Ñø",
    ),
    (
        "yea no",
        "Yea No",
    ),
    (
        "ýæ ñø",
        "Ýæ Ñø",
    ),
    # https://github.com/ppannuto/python-titlecase/pull/67
    (
        "Mr mr Mrs Ms Mss , Mr. and Mrs. Person",
        "Mr Mr Mrs Ms MSS , Mr. And Mrs. Person",
    ),
)

def test_initials_regex():
    """Test - uppercase initials regex with A.B"""
    from titlecase import UC_INITIALS
    assert bool(UC_INITIALS.match('A.B')) is True

def test_initials_regex_2():
    """Test - uppercase initials regex with A.B."""
    from titlecase import UC_INITIALS
    assert bool(UC_INITIALS.match('A.B.')) is True

def test_initials_regex_3():
    """Test - uppercase initials regex with ABCD"""
    from titlecase import UC_INITIALS
    assert bool(UC_INITIALS.match('ABCD')) is False

def check_input_matches_expected_output(in_, out):
    """Function yielded by test generator"""
    try:
        assert titlecase(in_) == out
    except AssertionError:
        print("{0} != {1}".format(titlecase(in_), out))
        raise

def test_at_and_t():
    def at_n_t(word, **kwargs):
        if word.upper() == "AT&T":
            return word.upper()
    print(titlecase("at&t", callback=at_n_t))
    assert titlecase("at&t", callback=at_n_t) == "AT&T"

def test_input_output():
    """Generated tests"""
    for data in TEST_DATA:
        yield check_input_matches_expected_output, data[0], data[1]

def test_callback():
    def abbreviation(word, **kwargs):
        if word.upper() in ('TCP', 'UDP'):
            return word.upper()
    s = 'a simple tcp and udp wrapper'
    # Note: this library is able to guess that all-consonant words are acronyms, so TCP
    # works naturally, but others will require the custom list
    assert titlecase(s) == 'A Simple TCP and Udp Wrapper'
    assert titlecase(s, callback=abbreviation) == 'A Simple TCP and UDP Wrapper'
    assert titlecase(s.upper(), callback=abbreviation) == 'A Simple TCP and UDP Wrapper'
    assert titlecase(u'crème brûlée', callback=lambda x, **kw: x.upper()) == u'CRÈME BRÛLÉE'

def test_set_small_word_list():
    assert titlecase('playing the game "words with friends"') == 'Playing the Game "Words With Friends"'
    set_small_word_list('a|an|the|with')
    assert titlecase('playing the game "words with friends"') == 'Playing the Game "Words with Friends"'

def test_custom_abbreviations():
    with tempfile.NamedTemporaryFile(mode='w') as f:
        f.write('UDP\nPPPoE\n')
        f.flush()
        # This works without a wordlist, because it begins mixed case
        assert titlecase('sending UDP packets over PPPoE works great') == 'Sending UDP Packets Over PPPoE Works Great'
        # Without a wordlist, this will do the "wrong" thing for the context
        assert titlecase('SENDING UDP PACKETS OVER PPPOE WORKS GREAT') == 'Sending Udp Packets Over Pppoe Works Great'
        # A wordlist can provide custom acronyms
        assert titlecase('sending UDP packets over PPPoE works great', callback=create_wordlist_filter_from_file(f.name)) == 'Sending UDP Packets Over PPPoE Works Great'

if __name__ == "__main__":
    import nose
    nose.main()
<|endoftext|>


<commit_before>'''
Module for calculating orbital correction for interferograms.

Created on 31/3/13
@author: Ben Davies
'''

from itertools import product
from numpy import sum, isnan, reshape, zeros, float32, vstack, squeeze
from scipy.linalg import lstsq
from numpy.linalg import pinv

import algorithm
from algorithm import ifg_date_lookup
from mst import default_mst

# Orbital correction tasks
#
# 4) forward calculation (orbfwd.m)
#		create 2D orbital correctionl layer from the model params

# TODO: options for multilooking
# 1) do the 2nd stage mlook at prepifg.py/generate up front, then delete in workflow after
# 2) refactor prep_ifgs() call to take input filenames and params & generate mlooked versions from that
#    this needs to be more generic to call at any point in the runtime.

# constants
INDEPENDENT_METHOD = 1
NETWORK_METHOD = 2

PLANAR = 1
QUADRATIC = 2

# TODO: do MST as a filter step outside the main func? More MODULAR?
# FIXME: operate on files

def orbital_correction(ifgs, degree, method, mlooked=None, offset=True):
	'''Top level method for correcting orbital error in the given Ifgs
	ifgs - list of Ifg objs to correct
	degree - PLANAR or QUADRATIC
	method - INDEPENDENT_METHOD or NETWORK_METHOD
	mlooked - sequence of multilooked Ifgs
	offset = True/False to include the constant/offset component
	'''
	# TODO: save corrected layers to new file or use intermediate arrays?

	if degree not in [PLANAR, QUADRATIC]:
		msg = "Invalid degree of %s for orbital correction" % degree
		raise OrbitalError(msg)

	if method not in [INDEPENDENT_METHOD, NETWORK_METHOD]:
		msg = "Unknown method '%s', need INDEPENDENT or NETWORK method" % method
		raise OrbitalError(msg)

	if method == NETWORK_METHOD:
		sub_ifgs = _subset_ifgs(ifgs) # reduce to the smallest tree with all nodes
		if mlooked:
			_validate_mlooked(mlooked, ifgs)
			sub_mlooked = _subset_mlooked_ifgs(sub_ifgs, mlooked)
			return _get_net_correction(sub_mlooked, degree, offset) # TODO: fwd corr
		else:
			return _get_net_correction(sub_ifgs, degree, offset) # TODO: fwd corr

	elif method == INDEPENDENT_METHOD:
		# FIXME: determine how to work this into the ifgs. Generate new Ifgs? Update
		# the old ifgs and flag the corrections in metadata?
		#
		#for i in ifgs:
		#	i.phase_data -= _get_ind_correction(i, degree, offset)
		return [_get_ind_correction(i, degree, offset) for i in ifgs]

def _validate_mlooked(mlooked, ifgs):
	'''Basic sanity checking of the multilooked ifgs.'''

	if len(mlooked) != len(ifgs):
		msg = "Mismatching # ifgs and # multilooked ifgs"
		raise OrbitalError(msg)

	tmp = [hasattr(i, 'phase_data') for i in mlooked]
	if all(tmp) is False:
		msg = "Mismatching types multilooked ifgs arg:\n%s" % mlooked
		raise OrbitalError(msg)

def _subset_ifgs(ifgs):
	mst = default_mst(ifgs)
	tmp = [ifg_date_lookup(ifgs, mas_slv) for mas_slv in mst.iteritems()]
	dates = [i.DATE12 for i in ifgs] # retain order of ifgs
	tmp = { i.DATE12 : i for i in tmp }
	return [tmp[d] for d in dates]

def _subset_mlooked_ifgs(ifgs, mlooked):
	'''
	Returns corresponding subset of multilooked ifgs from the subset 'ifgs' in the
	same order. Assumes mlooked has more elements than 'ifgs'. Order is retained.
	ifgs - subset of ifgs (eg. from main set of ifgs)
	mlooked - sequence of multilooked ifgs to subset
	'''
	dates = [e.DATE12 for e in ifgs] # retain date order of orig
	lk = {e.DATE12: e for e in ifgs}
	return [lk[d] for d in dates]

def _get_ind_correction(ifg, degree, offset):
	'''Calculates and returns orbital correction array for an ifg'''

	vphase = reshape(ifg.phase_data, ifg.num_cells) # vectorised, contains NODATA
	dm = get_design_matrix(ifg, degree, offset)
	assert len(vphase) == len(dm)

	# filter NaNs out before getting model
	tmp = dm[~isnan(vphase)]
	fd = vphase[~isnan(vphase)]
	model, _, rank, _ = lstsq(tmp, fd)
	exp_len = 2 if degree == PLANAR else 5
	if offset:
		exp_len += 1
	assert len(model) == exp_len

	# calculate forward model & morph back to 2D
	tmp = sum(dm * model, axis=1) # d = ax + by
	correction = reshape(tmp, ifg.phase_data.shape)
	return correction

def _get_net_correction(ifgs, degree, offset):
	'''Returns the TODO
	ifgs - assumed to be Ifgs from a prior MST step
	degree - PLANAR or QUADRATIC
	offset - True/False for including TODO
	'''

	# FIXME: correction needs to apply to *all* ifgs - can still get the correction
	#        as there are model params for each epoch (+ ifgs relate to all those epochs).
	# TODO: multilooking (do seperately/prior to this as a batch job?)

	# get DM / clear out the NaNs based on obs
	tmp = vstack([i.phase_data.reshape((i.num_cells, 1)) for i in ifgs])
	vphase = squeeze(tmp)
	dm = get_network_design_matrix(ifgs, degree, offset)
	assert len(vphase) == len(dm)

	# filter NaNs out before getting model
	tmp = dm[~isnan(vphase)]
	fd = vphase[~isnan(vphase)]
	model = pinv(tmp, 1e-6) * fd
	return model

def get_design_matrix(ifg, degree, offset):
	'''Returns design matrix with 2 columns for linear model parameters'''

	nparams = 2 if degree == PLANAR else 5
	if offset:
		nparams += 1  # eg. y = mx + offset

	# init design matrix
	shape = (ifg.num_cells, nparams)
	data = zeros(shape, dtype=float32)
	rows = iter(data)

	dmfun = _planar_dm if degree == PLANAR else _quadratic_dm
	dmfun(ifg, rows, offset)
	return data

# TODO: can this be refactored under one get_design_matrix() func?
def get_network_design_matrix(ifgs, degree, offset):
	'''Returns a larger format design matrix for networked error correction.'''

	if degree not in [PLANAR, QUADRATIC]:
		raise OrbitalError("Invalid degree argument")

	num_ifgs = len(ifgs)
	if num_ifgs < 1:
		# can feasibly do correction on a single Ifg/2 epochs
		raise OrbitalError("Invalid number of Ifgs")

	# TODO: refactor to prevent duplication with get_design_matrix()?
	nparams = 2 if degree == PLANAR else 5

	# sort out master and slave date IDs
	dates = [ifg.MASTER for ifg in ifgs] + [ifg.SLAVE for ifg in ifgs]
	ids = algorithm.master_slave_ids(dates)
	num_epochs = max(ids.values()) + 1 # convert from zero indexed ID
	np = nparams * num_epochs

	# init design matrix
	shape = [ifgs[0].num_cells * num_ifgs, np]
	if offset:
		shape[1] += num_ifgs # add extra cols for offset component
	data = zeros(shape, dtype=float32)

	# paste in individual design matrices
	for i, ifg in enumerate(ifgs):
		tmp = get_design_matrix(ifg, degree, False) # network method doesn't have extra col
		rs = i * ifg.num_cells
		rf = rs + ifg.num_cells

		# generate column indices into data based on master position
		mascs = ids[ifg.MASTER] * nparams
		mascf = mascs + nparams
		data[rs:rf, mascs:mascf] = -tmp

		# then for slave
		slvcs =	ids[ifg.SLAVE] * nparams
		slvcf = slvcs + nparams
		data[rs:rf, slvcs:slvcf] = tmp

		if offset:
			data[rs:rf, np + i] = 1  # add offset cols

	return data

def _planar_dm(ifg, rows, offset):
	# apply positional parameter values, multiply pixel coordinate by cell size to
	# get distance (a coord by itself doesn't tell us distance from origin)

	# TODO: optimise with meshgrid calls?
	# TODO: make more efficient by pre generating xranges and doing array ops?
	# TODO: coordinates generator for Ifgs?

	if offset:
		for y,x in product(xrange(ifg.FILE_LENGTH), xrange(ifg.WIDTH)):
			row = rows.next() # TODO: make faster with vstack?
			row[:] = [x * ifg.X_SIZE, y * ifg.Y_SIZE, 1]
	else:
		for y,x in product(xrange(ifg.FILE_LENGTH), xrange(ifg.WIDTH)):
			row = rows.next() # TODO: make faster with vstack?
			row[:] = [x * ifg.X_SIZE, y * ifg.Y_SIZE]

def _quadratic_dm(ifg, rows, offset):
	# apply positional parameter values, multiply pixel coordinate by cell size to
	# get distance (a coord by itself doesn't tell us distance from origin)
	yst, xst = ifg.Y_SIZE, ifg.X_SIZE

	# TODO: refactor, use ones +/- final col and paste these values over it
	if offset:
		for y,x in product(xrange(ifg.FILE_LENGTH), xrange(ifg.WIDTH)):
			row = rows.next()
			y2 = y * yst
			x2 = x * xst
			row[:] = [x2**2, y2**2, x2*y2, x2, y2, 1]
	else:
		for y,x in product(xrange(ifg.FILE_LENGTH), xrange(ifg.WIDTH)):
			row = rows.next()
			y2 = y * yst
			x2 = x * xst
			row[:] = [x2**2, y2**2, x2*y2, x2, y2]

class OrbitalError(Exception):
	'''Generic class for errors in orbital correction'''
	pass
<commit_msg>Refactoring: removed MST code for modularity and ease of testing.<commit_after>'''
Module for calculating orbital correction for interferograms.

Created on 31/3/13
@author: Ben Davies
'''

from itertools import product
from numpy import sum, isnan, reshape, zeros, float32, vstack, squeeze
from scipy.linalg import lstsq
from numpy.linalg import pinv

import algorithm
from algorithm import ifg_date_lookup
from mst import default_mst

# Orbital correction tasks
#
# 4) forward calculation (orbfwd.m)
#		create 2D orbital correctionl layer from the model params

# TODO: options for multilooking
# 1) do the 2nd stage mlook at prepifg.py/generate up front, then delete in workflow after
# 2) refactor prep_ifgs() call to take input filenames and params & generate mlooked versions from that
#    this needs to be more generic to call at any point in the runtime.

# constants
INDEPENDENT_METHOD = 1
NETWORK_METHOD = 2

PLANAR = 1
QUADRATIC = 2

# TODO: do MST as a filter step outside the main func? More MODULAR?
# FIXME: operate on files

def orbital_correction(ifgs, degree, method, mlooked=None, offset=True):
	'''
	TODO: Top level method for correcting orbital error in the given Ifgs. It is
	assumed the given ifgs have been reduced using MST for the network method.

	ifgs - list of Ifg objs to correct
	degree - PLANAR or QUADRATIC
	method - INDEPENDENT_METHOD or NETWORK_METHOD
	mlooked - sequence of multilooked Ifgs
	offset = True/False to include the constant/offset component
	'''
	# TODO: save corrected layers to new file or use intermediate arrays?

	if degree not in [PLANAR, QUADRATIC]:
		msg = "Invalid degree of %s for orbital correction" % degree
		raise OrbitalError(msg)

	if method not in [INDEPENDENT_METHOD, NETWORK_METHOD]:
		msg = "Unknown method '%s', need INDEPENDENT or NETWORK method" % method
		raise OrbitalError(msg)

	if method == NETWORK_METHOD:
		if mlooked:
			_validate_mlooked(mlooked, ifgs)
			return _get_net_correction(sub_mlooked, degree, offset) # TODO: fwd corr
		else:
			return _get_net_correction(ifgs, degree, offset) # TODO: fwd corr

	elif method == INDEPENDENT_METHOD:
		# FIXME: determine how to work this into the ifgs. Generate new Ifgs? Update
		# the old ifgs and flag the corrections in metadata?
		#
		#for i in ifgs:
		#	i.phase_data -= _get_ind_correction(i, degree, offset)
		return [_get_ind_correction(i, degree, offset) for i in ifgs]

def _validate_mlooked(mlooked, ifgs):
	'''Basic sanity checking of the multilooked ifgs.'''

	if len(mlooked) != len(ifgs):
		msg = "Mismatching # ifgs and # multilooked ifgs"
		raise OrbitalError(msg)

	tmp = [hasattr(i, 'phase_data') for i in mlooked]
	if all(tmp) is False:
		msg = "Mismatching types multilooked ifgs arg:\n%s" % mlooked
		raise OrbitalError(msg)

def _get_ind_correction(ifg, degree, offset):
	'''Calculates and returns orbital correction array for an ifg'''

	vphase = reshape(ifg.phase_data, ifg.num_cells) # vectorised, contains NODATA
	dm = get_design_matrix(ifg, degree, offset)
	assert len(vphase) == len(dm)

	# filter NaNs out before getting model
	tmp = dm[~isnan(vphase)]
	fd = vphase[~isnan(vphase)]
	model, _, rank, _ = lstsq(tmp, fd)
	exp_len = 2 if degree == PLANAR else 5
	if offset:
		exp_len += 1
	assert len(model) == exp_len

	# calculate forward model & morph back to 2D
	tmp = sum(dm * model, axis=1) # d = ax + by
	correction = reshape(tmp, ifg.phase_data.shape)
	return correction

def _get_net_correction(ifgs, degree, offset):
	'''
	Returns the TODO
	ifgs - assumed to be Ifgs from a prior MST step
	degree - PLANAR or QUADRATIC
	offset - True/False for including TODO
	'''

	# FIXME: correction needs to apply to *all* ifgs - can still get the correction
	#        as there are model params for each epoch (+ ifgs relate to all those epochs).
	# TODO: multilooking (do seperately/prior to this as a batch job?)

	# get DM / clear out the NaNs based on obs
	tmp = vstack([i.phase_data.reshape((i.num_cells, 1)) for i in ifgs])
	vphase = squeeze(tmp)
	dm = get_network_design_matrix(ifgs, degree, offset)
	assert len(vphase) == len(dm)

	# filter NaNs out before getting model
	tmp = dm[~isnan(vphase)]
	fd = vphase[~isnan(vphase)]
	model = pinv(tmp, 1e-6) * fd
	return model

def get_design_matrix(ifg, degree, offset):
	'''Returns design matrix with 2 columns for linear model parameters'''

	nparams = 2 if degree == PLANAR else 5
	if offset:
		nparams += 1  # eg. y = mx + offset

	# init design matrix
	shape = (ifg.num_cells, nparams)
	data = zeros(shape, dtype=float32)
	rows = iter(data)

	dmfun = _planar_dm if degree == PLANAR else _quadratic_dm
	dmfun(ifg, rows, offset)
	return data

# TODO: can this be refactored under one get_design_matrix() func?
def get_network_design_matrix(ifgs, degree, offset):
	'''Returns a larger format design matrix for networked error correction.'''

	if degree not in [PLANAR, QUADRATIC]:
		raise OrbitalError("Invalid degree argument")

	num_ifgs = len(ifgs)
	if num_ifgs < 1:
		# can feasibly do correction on a single Ifg/2 epochs
		raise OrbitalError("Invalid number of Ifgs")

	# TODO: refactor to prevent duplication with get_design_matrix()?
	nparams = 2 if degree == PLANAR else 5

	# sort out master and slave date IDs
	dates = [ifg.MASTER for ifg in ifgs] + [ifg.SLAVE for ifg in ifgs]
	ids = algorithm.master_slave_ids(dates)
	num_epochs = max(ids.values()) + 1 # convert from zero indexed ID
	np = nparams * num_epochs

	# init design matrix
	shape = [ifgs[0].num_cells * num_ifgs, np]
	if offset:
		shape[1] += num_ifgs # add extra cols for offset component
	data = zeros(shape, dtype=float32)

	# paste in individual design matrices
	for i, ifg in enumerate(ifgs):
		tmp = get_design_matrix(ifg, degree, False) # network method doesn't have extra col
		rs = i * ifg.num_cells
		rf = rs + ifg.num_cells

		# generate column indices into data based on master position
		mascs = ids[ifg.MASTER] * nparams
		mascf = mascs + nparams
		data[rs:rf, mascs:mascf] = -tmp

		# then for slave
		slvcs =	ids[ifg.SLAVE] * nparams
		slvcf = slvcs + nparams
		data[rs:rf, slvcs:slvcf] = tmp

		if offset:
			data[rs:rf, np + i] = 1  # add offset cols

	return data

def _planar_dm(ifg, rows, offset):
	# apply positional parameter values, multiply pixel coordinate by cell size to
	# get distance (a coord by itself doesn't tell us distance from origin)

	# TODO: optimise with meshgrid calls?
	# TODO: make more efficient by pre generating xranges and doing array ops?
	# TODO: coordinates generator for Ifgs?

	if offset:
		for y,x in product(xrange(ifg.FILE_LENGTH), xrange(ifg.WIDTH)):
			row = rows.next() # TODO: make faster with vstack?
			row[:] = [x * ifg.X_SIZE, y * ifg.Y_SIZE, 1]
	else:
		for y,x in product(xrange(ifg.FILE_LENGTH), xrange(ifg.WIDTH)):
			row = rows.next() # TODO: make faster with vstack?
			row[:] = [x * ifg.X_SIZE, y * ifg.Y_SIZE]

def _quadratic_dm(ifg, rows, offset):
	# apply positional parameter values, multiply pixel coordinate by cell size to
	# get distance (a coord by itself doesn't tell us distance from origin)
	yst, xst = ifg.Y_SIZE, ifg.X_SIZE

	# TODO: refactor, use ones +/- final col and paste these values over it
	if offset:
		for y,x in product(xrange(ifg.FILE_LENGTH), xrange(ifg.WIDTH)):
			row = rows.next()
			y2 = y * yst
			x2 = x * xst
			row[:] = [x2**2, y2**2, x2*y2, x2, y2, 1]
	else:
		for y,x in product(xrange(ifg.FILE_LENGTH), xrange(ifg.WIDTH)):
			row = rows.next()
			y2 = y * yst
			x2 = x * xst
			row[:] = [x2**2, y2**2, x2*y2, x2, y2]

class OrbitalError(Exception):
	'''Generic class for errors in orbital correction'''
	pass
<|endoftext|>


<commit_before># Copyright (c) 2015, Daniele Venzano
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from zoe_lib.configargparse import ArgumentParser, Namespace

config_paths = [
    'zoe-web.conf',
    '/etc/zoe/zoe-web.conf'
]

_conf = None

def load_configuration():
    global _conf
    argparser = ArgumentParser(description="Zoe Web interface: Container Analytics as a Service web component",
                               default_config_files=config_paths,
                               auto_env_var_prefix="ZOE_WEB_",
                               args_for_setting_config_path=["--config"],
                               args_for_writing_out_config_file=["--write-config"])
    argparser.add_argument('--debug', action='store_true', help='Enable debug output')
    argparser.add_argument('--listen-address', type=str, help='Address to listen to for incoming connections', default="0.0.0.0")
    argparser.add_argument('--listen-port', type=int, help='Port to listen to for incoming connections', default=5001)
    argparser.add_argument('--zoe-url', help='URL of the Zoe master process', default='http://127.0.0.1:4850')
    argparser.add_argument('--zoe-admin-user', help='Admin-level user in Zoe', default='zoeadmin')
    argparser.add_argument('--zoe-admin-pass', help='Password for the admin-level user in Zoe', default='changeme')
    argparser.add_argument('--cookie-secret', help='key used to encrypt cookies', default="hr4h3H'kmn F8fz/;CJN5a!")

    opts = argparser.parse_args()
    if opts.debug:
        argparser.print_values()
    _conf = opts

def get_conf() -> Namespace:
    return _conf
<commit_msg>Remove unused configuration options from the web client<commit_after># Copyright (c) 2015, Daniele Venzano
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#    http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from zoe_lib.configargparse import ArgumentParser, Namespace

config_paths = [
    'zoe-web.conf',
    '/etc/zoe/zoe-web.conf'
]

_conf = None

def load_configuration():
    global _conf
    argparser = ArgumentParser(description="Zoe Web interface: Container Analytics as a Service web component",
                               default_config_files=config_paths,
                               auto_env_var_prefix="ZOE_WEB_",
                               args_for_setting_config_path=["--config"],
                               args_for_writing_out_config_file=["--write-config"])
    argparser.add_argument('--debug', action='store_true', help='Enable debug output')
    argparser.add_argument('--listen-address', type=str, help='Address to listen to for incoming connections', default="0.0.0.0")
    argparser.add_argument('--listen-port', type=int, help='Port to listen to for incoming connections', default=5001)
    argparser.add_argument('--zoe-url', help='URL of the Zoe master process', default='http://127.0.0.1:4850')
    argparser.add_argument('--cookie-secret', help='key used to encrypt cookies', default="hr4h3H'kmn F8fz/;CJN5a!")

    opts = argparser.parse_args()
    if opts.debug:
        argparser.print_values()
    _conf = opts

def get_conf() -> Namespace:
    return _conf
<|endoftext|>


<commit_before>import inspect
from pyrules2.expression import ReferenceExpression, bind
from itertools import islice
from functools import partial

class VirtualSelf(object):
    pass

class Var(object):
    def __init__(self, name):
        assert isinstance(name, str)
        self.variable_name = name

    def __repr__(self):
        return 'Var({})'.format(self.variable_name)

ANYTHING = object()

class InternalExpressionMethod(object):
    def __init__(self, rule_method, reference_expression):
        """
        Constructs a callable to replace the given method.
        The callable will generate all dicts for the given expression,
        renamed and filtered using the pyrules.bind() function.
        :param rule_method: A @rule method from a RuleBook
        :param reference_expression: The ReferenceExpression that will be used
        to represent the body of the rule_method
        :return: The constructed method.
        """
        self.rule_method = rule_method
        assert isinstance(reference_expression, ReferenceExpression)
        self.reference_expression = reference_expression

    def __call__(self, *args):
        call_args = inspect.getcallargs(self.rule_method, None, *args)
        assert call_args['self'] is None
        del call_args['self']
        const_bindings = {}
        var_bindings = {}
        for arg_name, arg_value in call_args.iteritems():
            if isinstance(arg_value, Var):
                var_bindings[arg_name] = arg_value.variable_name
            elif arg_value is ANYTHING:
                var_bindings[arg_name] = arg_name
            else:
                const_bindings[arg_name] = arg_value
        return bind(callee_expr=self.reference_expression,
                    callee_key_to_constant=const_bindings,
                    callee_key_to_caller_key=var_bindings)

    def __repr__(self):
        return '{}({!r},{!r})'.format(self.__class__.__name__,
                                      self.rule_method,
                                      self.reference_expression)

class ExternalExpressionMethod(InternalExpressionMethod):
    """
    Like InternalExpressionMethod, but
      - Does not return the Expression but rather a generator of dicts.
      - Limits the number of results to the page_size attribute
        of the RuleBook.
    Note that ExternalExpressionMethod is a descriptor object.
    This allows it to access the RuleBook object it is member of.
    """
    def __call__(self, rule_book, *args):
        """
        Note, because of the descriptor protocol, this method will
        not be called directly, but through __get__() below.
        """
        expression = super(ExternalExpressionMethod, self).__call__(*args)
        # TODO: Give access to page 2. Workaround: Increase page size.
        assert isinstance(rule_book, RuleBook) or isinstance(rule_book, FixedPointRuleBook)
        return islice(expression.all_dicts(), rule_book.page_size)

    def __get__(self, instance, instancetype):
        """
        Implementation of the descriptor protocol.
        :return self.__call__ with its first argument bound to the
        RuleBook instance given.
        """
        return partial(self.__call__, instance)

def rewrite(rules):
    reference_expressions = {rule_name: ReferenceExpression(rule_name) for rule_name in rules}
    vs = VirtualSelf()
    for rule_name, rule_method in rules.items():
        setattr(vs, rule_name, InternalExpressionMethod(rule_method, reference_expressions[rule_name]))
    for rule_name, rule_method in rules.items():
        arg_names = inspect.getargspec(rule_method).args
        assert arg_names[0] == 'self'
        vars_for_non_self_args = [Var(arg) for arg in arg_names[1:]]
        generated_expression = rule_method(vs, *vars_for_non_self_args)
        reference_expressions[rule_name].set_expression(generated_expression)
    for rule_name, rule_method in rules.items():
        rules[rule_name] = ExternalExpressionMethod(rule_method, reference_expressions[rule_name])
    # Store an index of the rules
    rules['__index__'] = reference_expressions

class StdMeta(type):
    def __new__(mcs, name, bases, class_dict):
        rules = {key: value for key, value in class_dict.items() if hasattr(value, 'pyrules')}
        rewrite(rules)
        class_dict.update(rules)
        cls = type.__new__(mcs, name, bases, class_dict)
        return cls

    def __str__(self):
        return '\n'.join('{}:\n{}'.format(rule_name, reference_expression.ref) for rule_name, reference_expression in self.__index__.items())

class RuleBook(object):
    """
    A RuleBook combines a number of rules, i.e. methods decorated with @rule,
    and answers queries to these. When an instance of the RuleBook is
    constructed, every @rule is parsed.
    """
    __metaclass__ = StdMeta

    def __init__(self):
        # The maximum number of results to generate when calling a rule in this RuleBook
        self.page_size = 1000

from itertools import count
from pyrules2.expression import when, Expression

class VirtualMethod(object):
    def __init__(self, rule_method, expression):
        """
        Constructs a callable to replace the given method.
        The callable will generate all dicts for the given expression,
        renamed and filtered using the pyrules.bind() function.
        :param rule_method: A @rule method from a RuleBook
        :param expression: The Expression that will be used
        to represent the body of the rule_method
        :return: The constructed method.
        """
        self.rule_method = rule_method
        assert isinstance(expression, Expression)
        self.expression = expression

    def __call__(self, *args):
        call_args = inspect.getcallargs(self.rule_method, None, *args)
        assert call_args['self'] is None
        del call_args['self']
        const_bindings = {}
        var_bindings = {}
        for arg_name, arg_value in call_args.iteritems():
            if isinstance(arg_value, Var):
                var_bindings[arg_name] = arg_value.variable_name
            elif arg_value is ANYTHING:
                var_bindings[arg_name] = arg_name
            else:
                const_bindings[arg_name] = arg_value
        return bind(callee_expr=self.expression,
                    callee_key_to_constant=const_bindings,
                    callee_key_to_caller_key=var_bindings)

    def __repr__(self):
        return '{}({!r},{!r})'.format(self.__class__.__name__,
                                      self.rule_method,
                                      self.expression)

class FixedPointMethod(object):  # TODO: AssignableGeneratorMethod?
    """
    TODO
    """
    def __init__(self, name):
        self.name = name

    def __call__(self, rule_book, *args):
        """
        Note, because of the descriptor protocol, this method will
        not be called directly, but through __get__() below.
        """
        assert isinstance(rule_book, FixedPointRuleBook)
        seen = set()
        for i in count(1):
            print 'Requesting {}@{}'.format(self.name, i)
            unbound_expression = rule_book.generation(self.name, i)
            bound_expression = self.bind_args_expression(rule_book, args,unbound_expression)
            added = set()
            for scenario in bound_expression.scenarios():
                print '{}: Considering {}'.format(self.name, scenario)
                if scenario not in seen:
                    added.add(scenario)
                    yield scenario.as_dict()
            if len(added) == 0:  # TODO: This should be for all keys, not just this one!
                return
            else:
                seen |= added

    def bind_args_expression(self, rule_book, args, unbound_expression):
        call_args = inspect.getcallargs(rule_book.__class__.__original_rules__[self.name], None, *args)
        assert call_args['self'] is None
        del call_args['self']
        const_bindings = {}
        var_bindings = {}
        for arg_name, arg_value in call_args.iteritems():
            if isinstance(arg_value, Var):
                var_bindings[arg_name] = arg_value.variable_name
            elif arg_value is ANYTHING:
                var_bindings[arg_name] = arg_name
            else:
                const_bindings[arg_name] = arg_value
        return bind(callee_expr=unbound_expression,
                    callee_key_to_constant=const_bindings,
                    callee_key_to_caller_key=var_bindings)

    def __get__(self, instance, instancetype):
        """
        Implementation of the descriptor protocol.
        :return self.__call__ with its first argument bound to the
        RuleBook instance given.
        """
        return partial(self.__call__, instance)

class FixedPointMeta(type):
    def __new__(mcs, name, bases, class_dict):
        rules = {key: value for key, value in class_dict.items() if hasattr(value, 'pyrules')}
        class_dict['__original_rules__'] = rules
        for key in rules:
            class_dict[key] = FixedPointMethod(key)
        cls = type.__new__(mcs, name, bases, class_dict)
        return cls

class Done(RuntimeError):
    pass

_EMPTY_EXPRESSION = when(x=0) & when(x=1)

class FixedPointRuleBook(object):
    """
    A RuleBook combines a number of rules, i.e. methods decorated with @rule,
    and answers queries to these. When an instance of the RuleBook is
    constructed, every @rule is parsed
    """
    __metaclass__ = FixedPointMeta

    def __init__(self):
        pass

    def rules(self):
        return self.__class__.__original_rules__

    def generation(self, key, generation_no):
        assert key in self.rules()
        assert generation_no >= 0
        if generation_no == 0:
            return _EMPTY_EXPRESSION
        previous_generation = {key: self.generation(key, generation_no-1) for key in self.rules()}
        expression_for_key = self.parse(key, previous_generation)
        return expression_for_key

    def parse(self, key, environment):
        vs = VirtualSelf()
        for rule_name in self.rules():
            setattr(vs, rule_name, VirtualMethod(self.rules()[rule_name],
                                                 environment[rule_name]))
        arg_names = inspect.getargspec(self.rules()[key]).args
        assert arg_names[0] == 'self'
        vars_for_non_self_args = [Var(arg) for arg in arg_names[1:]]
        return self.rules()[key](vs, *vars_for_non_self_args)

def rule(func):
    """
    Example usage:
    @rule # Call p like this: for y in r.p(atom.A, var.y): print y
    def p(self, x, y):
        return matches([(atom.A, atom.B), (atom.C, atom.D)], x, y)
    :param func: A method returning a program.
    :return: Solutions
    """
    func.pyrules = True
    return func

<commit_msg>Fixed bug in detecting fixed point<commit_after>import inspect
from pyrules2.expression import ReferenceExpression, bind
from itertools import islice
from functools import partial

class VirtualSelf(object):
    pass

class Var(object):
    def __init__(self, name):
        assert isinstance(name, str)
        self.variable_name = name

    def __repr__(self):
        return 'Var({})'.format(self.variable_name)

ANYTHING = object()

class InternalExpressionMethod(object):
    def __init__(self, rule_method, reference_expression):
        """
        Constructs a callable to replace the given method.
        The callable will generate all dicts for the given expression,
        renamed and filtered using the pyrules.bind() function.
        :param rule_method: A @rule method from a RuleBook
        :param reference_expression: The ReferenceExpression that will be used
        to represent the body of the rule_method
        :return: The constructed method.
        """
        self.rule_method = rule_method
        assert isinstance(reference_expression, ReferenceExpression)
        self.reference_expression = reference_expression

    def __call__(self, *args):
        call_args = inspect.getcallargs(self.rule_method, None, *args)
        assert call_args['self'] is None
        del call_args['self']
        const_bindings = {}
        var_bindings = {}
        for arg_name, arg_value in call_args.iteritems():
            if isinstance(arg_value, Var):
                var_bindings[arg_name] = arg_value.variable_name
            elif arg_value is ANYTHING:
                var_bindings[arg_name] = arg_name
            else:
                const_bindings[arg_name] = arg_value
        return bind(callee_expr=self.reference_expression,
                    callee_key_to_constant=const_bindings,
                    callee_key_to_caller_key=var_bindings)

    def __repr__(self):
        return '{}({!r},{!r})'.format(self.__class__.__name__,
                                      self.rule_method,
                                      self.reference_expression)

class ExternalExpressionMethod(InternalExpressionMethod):
    """
    Like InternalExpressionMethod, but
      - Does not return the Expression but rather a generator of dicts.
      - Limits the number of results to the page_size attribute
        of the RuleBook.
    Note that ExternalExpressionMethod is a descriptor object.
    This allows it to access the RuleBook object it is member of.
    """
    def __call__(self, rule_book, *args):
        """
        Note, because of the descriptor protocol, this method will
        not be called directly, but through __get__() below.
        """
        expression = super(ExternalExpressionMethod, self).__call__(*args)
        # TODO: Give access to page 2. Workaround: Increase page size.
        assert isinstance(rule_book, RuleBook) or isinstance(rule_book, FixedPointRuleBook)
        return islice(expression.all_dicts(), rule_book.page_size)

    def __get__(self, instance, instancetype):
        """
        Implementation of the descriptor protocol.
        :return self.__call__ with its first argument bound to the
        RuleBook instance given.
        """
        return partial(self.__call__, instance)

def rewrite(rules):
    reference_expressions = {rule_name: ReferenceExpression(rule_name) for rule_name in rules}
    vs = VirtualSelf()
    for rule_name, rule_method in rules.items():
        setattr(vs, rule_name, InternalExpressionMethod(rule_method, reference_expressions[rule_name]))
    for rule_name, rule_method in rules.items():
        arg_names = inspect.getargspec(rule_method).args
        assert arg_names[0] == 'self'
        vars_for_non_self_args = [Var(arg) for arg in arg_names[1:]]
        generated_expression = rule_method(vs, *vars_for_non_self_args)
        reference_expressions[rule_name].set_expression(generated_expression)
    for rule_name, rule_method in rules.items():
        rules[rule_name] = ExternalExpressionMethod(rule_method, reference_expressions[rule_name])
    # Store an index of the rules
    rules['__index__'] = reference_expressions

class StdMeta(type):
    def __new__(mcs, name, bases, class_dict):
        rules = {key: value for key, value in class_dict.items() if hasattr(value, 'pyrules')}
        rewrite(rules)
        class_dict.update(rules)
        cls = type.__new__(mcs, name, bases, class_dict)
        return cls

    def __str__(self):
        return '\n'.join('{}:\n{}'.format(rule_name, reference_expression.ref) for rule_name, reference_expression in self.__index__.items())

class RuleBook(object):
    """
    A RuleBook combines a number of rules, i.e. methods decorated with @rule,
    and answers queries to these. When an instance of the RuleBook is
    constructed, every @rule is parsed.
    """
    __metaclass__ = StdMeta

    def __init__(self):
        # The maximum number of results to generate when calling a rule in this RuleBook
        self.page_size = 1000

from itertools import count
from pyrules2.expression import when, Expression

class VirtualMethod(object):
    def __init__(self, rule_method, expression):
        """
        Constructs a callable to replace the given method.
        The callable will generate all dicts for the given expression,
        renamed and filtered using the pyrules.bind() function.
        :param rule_method: A @rule method from a RuleBook
        :param expression: The Expression that will be used
        to represent the body of the rule_method
        :return: The constructed method.
        """
        self.rule_method = rule_method
        assert isinstance(expression, Expression)
        self.expression = expression

    def __call__(self, *args):
        call_args = inspect.getcallargs(self.rule_method, None, *args)
        assert call_args['self'] is None
        del call_args['self']
        const_bindings = {}
        var_bindings = {}
        for arg_name, arg_value in call_args.iteritems():
            if isinstance(arg_value, Var):
                var_bindings[arg_name] = arg_value.variable_name
            elif arg_value is ANYTHING:
                var_bindings[arg_name] = arg_name
            else:
                const_bindings[arg_name] = arg_value
        return bind(callee_expr=self.expression,
                    callee_key_to_constant=const_bindings,
                    callee_key_to_caller_key=var_bindings)

    def __repr__(self):
        return '{}({!r},{!r})'.format(self.__class__.__name__,
                                      self.rule_method,
                                      self.expression)

class FixedPointMethod(object):  # TODO: AssignableGeneratorMethod?
    """
    TODO
    """
    def __init__(self, name):
        self.name = name

    def __call__(self, rule_book, *args):
        """
        Note, because of the descriptor protocol, this method will
        not be called directly, but through __get__() below.
        """
        assert isinstance(rule_book, FixedPointRuleBook)
        seen = set()
        try:
            for i in count(1):
                print 'Requesting {}@{}'.format(self.name, i)
                unbound_expression = rule_book.generation(self.name, i)
                bound_expression = self.bind_args_expression(rule_book, args,unbound_expression)
                for scenario in bound_expression.scenarios():
                    if scenario not in seen:
                        yield scenario.as_dict()
        except Done:
            return

    def bind_args_expression(self, rule_book, args, unbound_expression):
        call_args = inspect.getcallargs(rule_book.__class__.__original_rules__[self.name], None, *args)
        assert call_args['self'] is None
        del call_args['self']
        const_bindings = {}
        var_bindings = {}
        for arg_name, arg_value in call_args.iteritems():
            if isinstance(arg_value, Var):
                var_bindings[arg_name] = arg_value.variable_name
            elif arg_value is ANYTHING:
                var_bindings[arg_name] = arg_name
            else:
                const_bindings[arg_name] = arg_value
        return bind(callee_expr=unbound_expression,
                    callee_key_to_constant=const_bindings,
                    callee_key_to_caller_key=var_bindings)

    def __get__(self, instance, instancetype):
        """
        Implementation of the descriptor protocol.
        :return self.__call__ with its first argument bound to the
        RuleBook instance given.
        """
        return partial(self.__call__, instance)

class FixedPointMeta(type):
    def __new__(mcs, name, bases, class_dict):
        rules = {key: value for key, value in class_dict.items() if hasattr(value, 'pyrules')}
        class_dict['__original_rules__'] = rules
        for key in rules:
            class_dict[key] = FixedPointMethod(key)
        cls = type.__new__(mcs, name, bases, class_dict)
        return cls

class Done(RuntimeError):
    pass

_EMPTY_EXPRESSION = when(x=0) & when(x=1)

class Generation(object):
    def __init__(self, keys):
        self.keys = frozenset(keys)
        self.expressions = {}
        self.frozensets = {}

    def set(self, key, expression):
        assert isinstance(expression, Expression), '{!r} should have been an Expression'.format(expression)
        assert key in self.keys
        assert key not in self.expressions
        assert key not in self.frozensets
        self.expressions[key] = expression
        self.frozensets[key] = frozenset(expression.scenarios())

    def __eq__(self, other):
        assert isinstance(other, Generation)
        assert self.is_full()
        assert other.is_full()
        return self.frozensets == other.frozensets

    def get_expression(self, key, callback=None):
        if key not in self.expressions:
            assert callback is not None
            self.set(key, callback(key))
        return self.expressions[key]

    def as_environment(self):
        return self.expressions

    def fill(self, callback):
        for key in self.keys:
            if key not in self.expressions:
                self.set(key, callback(key))

    def is_full(self):
        return self.keys == set(self.expressions.keys())

    def __repr__(self):
        if self.is_full():
            return '<{} full {!r}>'.format(self.__class__.__name__, self.frozensets)
        else:
            return '<{} missing={!r} found={!r}>'.format(self.__class__.__name__, self.keys.difference(self.expressions.keys()), self.frozensets)

class FixedPointRuleBook(object):
    """
    A RuleBook combines a number of rules, i.e. methods decorated with @rule,
    and answers queries to these. When an instance of the RuleBook is
    constructed, every @rule is parsed
    """
    __metaclass__ = FixedPointMeta

    def __init__(self):
        gen0 = Generation(self.rules().keys())
        gen0.fill(lambda key: _EMPTY_EXPRESSION)
        self.generations = [gen0]
        self.fixed_point = None

    def rules(self):
        return self.__class__.__original_rules__  # TODO: Copy

    def generation(self, key, generation_no):
        assert key in self.rules()
        if self.fixed_point is not None and generation_no > self.fixed_point:
            raise Done()
        assert generation_no > 0
        assert generation_no <= len(self.generations)
        if generation_no == len(self.generations):
            last_gen = self.generations[-1]
            next_gen = Generation(self.rules().keys())
            next_gen.fill(lambda key: self.parse(key, last_gen.as_environment()))
            if last_gen == next_gen:
                self.fixed_point = len(self.generations) - 1
            else:
                self.generations.append(next_gen)
        if self.fixed_point is not None and generation_no > self.fixed_point:
            raise Done()
        return self.generations[generation_no].get_expression(key)

    def parse(self, key, environment):
        vs = VirtualSelf()
        for rule_name in self.rules():
            setattr(vs, rule_name, VirtualMethod(self.rules()[rule_name],
                                                 environment[rule_name]))
        arg_names = inspect.getargspec(self.rules()[key]).args
        assert arg_names[0] == 'self'
        vars_for_non_self_args = [Var(arg) for arg in arg_names[1:]]
        return self.rules()[key](vs, *vars_for_non_self_args)

def rule(func):
    """
    Example usage:
    @rule # Call p like this: for y in r.p(atom.A, var.y): print y
    def p(self, x, y):
        return matches([(atom.A, atom.B), (atom.C, atom.D)], x, y)
    :param func: A method returning a program.
    :return: Solutions
    """
    func.pyrules = True
    return func

<|endoftext|>