CANDIDATE_NAMES = """\
Ann
Bob
Carol
Dave
Ellen
Fred
Gwen
Hank
Irene
Joe
Katy
Leo
""".split()
STOP_CHOICE = object()
def make_standard_candidate_names(count, names=None):
    def __init__(self, choices, max_length=None, undervote=0.1):
    def choose(self, choices):
    def after_choice(self, choices, choice):
    def make_choices(self):
    def add_random_ballots(self, ballots_resource, count):
    def after_choice(self, choices, choice):
    def make_notes(self, candidate_count, ballot_count):
    def create_random(self, ballots_resource, candidate_count=None, ballot_count=None):
class BallotGenerator(object):
class UniqueBallotGenerator(BallotGenerator):
class ContestCreator(object): def any_value(dict_):
def get_majority(total):
def get_winner(totals):
def get_lowest(totals):
def count_irv_contest(contest):
    def __init__(self, contest):
    def count_ballots(self, candidate_numbers):
    def count(self):
class Tabulator(object): class JsonCaseConstants(JsonableMixin):
class JsonCaseBallot(JsonableMixin):
class JsonCaseContestInput(JsonableMixin):
class JsonCaseContestsFile(JsonableMixin):
class JsonCaseRoundResult(JsonableMixin):
class JsonCaseTestOutput(JsonableMixin):
class JsonCaseTestInstance(JsonableMixin):
class JsonCaseTestsFile(JsonableMixin):
    def model_to_kwargs(cls, model_obj):
    def __init__(self, choices=None, weight=1):
    def repr_info(self):
    def save_from_model(self, ballot):
    def to_model(self):
    def save_from_jsobj(self, jsobj):
    def to_jsobj(self):
    def repr_info(self):
    def make_candidate_names(self):
    def save_from_model(self, contest):
    def to_model(self):
    def save_attrs_to_jsobj(self, jsobj, names, convert=lambda x: x):
    def numbers_to_names(self, numbers):
    def totals_to_named_totals(self, totals):
    def to_jsobj(self): LIST_TYPES = (list, tuple, type(0 for i in ()))
JS_NULL = JsNull()
class JsNull(object):
class JsonPathInfo(PathInfo):
class JsonableError(Exception):
class JsonDeserializeError(Exception):
class Attribute(object):
class JsonableMixin(ReprMixin):
def call_json(json_func, *args, **kwargs):
def to_json(jsobj):
def read_json_path(path):
def write_json(obj, resource=None, path=None):
def from_model(obj, cls):
def from_jsobj(jsobj, cls=None):
def to_jsobj(obj):
    def __init__(self, path):
    def __init__(self, name, cls=None, keyword=None, model=True):
    def model_value(self, model_obj):
    def attrs(cls):
    def model_attrs(cls):
    def keywords_to_attrs(cls):
    def keywords_to_init_defaults(cls):
    def model_to_kwargs(cls, model_obj):
    def __init__(self, **kwargs):
    def repr_info(self):
    def return_false(self, *args):
    def raise_not_equal(self, other, value_desc, self_value, other_value):
    def _check_eq(self, other, not_equal_func):
    def assert_equal(self, other):
    def __eq__(self, other):
    def __ne__(self, other):
    def _attrs_from_jsdict(self, attrs, jsdict):
    def _attrs_to_jsdict(self, attrs, jsdict):
    def save_from_jsobj(self, jsobj):
    def get_meta_dict(self):
    def from_model(cls, model_obj):
    def save_from_model(self, model_obj):
    def to_model(self):
    def from_jsobj(cls, jsobj):
    def to_jsobj(self):
    def to_json(self): def make_candidate_numbers(candidate_count):
def normalize_ballots_to(source, target):
def normalize_ballots(ballots_resource):
    def count_ballots(self):
    def normalize(self):
    def __init__(self, candidates):
    def from_number(self, number):
    def from_numbers(self, numbers):
    def __init__(self, name=None, notes=None, candidates=None, seat_count=None,
                 ballots_resource=None):
    def repr_info(self):
    def make_candidates_info(self):
    def get_candidate_numbers(self):
    def should_normalize_ballots(self):
    def __init__(self, interrupted=None):
    def __init__(self, candidates_info=None, elected=None, eliminated=None,
                 tied_last_place=None, totals=None, tie_break=None):
    def __init__(self, outcome=None, rounds=None):
    def repr_info(self):
class BallotsResourceMixin(object):
class BallotsResource(streams.WrapperResource, BallotsResourceMixin):
class CandidatesInfo(object):
class ContestInput(ReprMixin):
class ContestOutcome(object):
class RoundResults(object):
class ContestResults(ReprMixin): ENCODING_INTERNAL_BALLOTS = "ascii"
ENCODING_JSON = "utf-8"
FILE_ENCODING = "utf-8"
def coroutine(func):
    def start(*args, **kwargs):
def fill(text, width=70):
def join_values(values):
def parse_integer_line(line):
def log_create_dir(path):
def logged_open(*args, **kwargs):
def make_dirs(path):
def ensure_dir(path):
def make_temp_dirname(name=None):
def temp_dir(path):
def temp_dir_inside(parent_dir):
def time_it(description):
    def __init__(self, obj):
    def __str__(self):
    def __repr__(self):
    def repr_info(self):
    def __init__(self, obj):
    def __getattr__(self, name):
    def repr_info(self):
    def close(self):
    def open_object(self, mode):
    def open(self, mode=None):
    def __init__(self, path, *args, **kwargs):
    def open_object(self, mode):
    def __init__(self, initial_value=None):
    def value(self):
    def repr_info(self):
    def get_display_value(self, limit=None):
    def open_object(self, mode):
    def __init__(self, resource):
    def open(self):
    def writeln(self, line):
class NoImplementation(NotImplementedError):
class ReprMixin(object):
class ObjectExtension(ReprMixin):
class UncloseableFile(ObjectExtension):
class StreamInfo(ReprMixin):
class PathInfo(StreamInfo):
class StringInfo(StreamInfo):
class FileWriter(object): ENCODING_JSON = 'utf-8'
class JsonCaseFormat(Format):
class JsonCaseContestWriter(FormatWriter):
    def contest_writer_cls(self):
    def get_output_infos(self):
    def get_output_info(self, output_dir):
    def resource_write(self, resource, contest): class JsonCaseContestInput(JsonableMixin):

    """Contest input for a JSON test case.

    Attributes (metadata):
      normalize_ballots: None means True.  Defaults to None.

    Attributes:
      ballots: an iterable of JsonCaseBallot objects.
      candidate_count: integer number of candidates.
    """

    meta_attrs = (Attribute('id', model=False),
                  Attribute('index', model=False),
                  Attribute('name'),
                  Attribute('normalize_ballots', model=False),
                  Attribute('rule_sets', model=False),
                  Attribute('notes'), )
    data_attrs = (Attribute('ballots', cls=JsonCaseBallot, model=False),
                  Attribute('candidate_count', model=False),
                  # TODO: make model=True.
                  Attribute('tie_elimination_order', model=False), )

    def repr_info(self):
        return "index=%s id=%s" % (self.index, self.id)

    def make_candidate_names(self):
        return contestgen.make_standard_candidate_names(self.candidate_count)

    # TODO: DRY this up by making last two lines part of base class.
    def save_from_model(self, contest):
        """
        Arguments:
          contest: a ContestInput object.
        """
        candidate_count = None if contest.candidates is None else len(contest.candidates)
        with contest.ballots_resource.reading() as ballots:
            ballots = [JsonCaseBallot.from_model(b) for b in ballots]
        kwargs = self.model_to_kwargs(contest)
        self.__init__(candidate_count=candidate_count, ballots=ballots, **kwargs)

    # TODO: think about how the creation of a new ballots resource should
    # be handled, since it involves managing another resource.
    # TODO: DRY this up by making last two lines part of base class.
    def to_model(self):
        """Return a ContestInput object."""
        candidates = self.make_candidate_names()
        ballots = [b.to_model() for b in self.ballots]
        # We use a list resource as the backing store for now because the
        # number of ballots is small.
        resource = streams.ListResource(ballots)
        ballots_resource = models.BallotsResource(resource)
        kwargs = self.model_to_kwargs(self)
        contest = models.ContestInput(candidates=candidates,
                                      ballots_resource=ballots_resource, **kwargs)
        return contest

    # def save_from_jsobj(self, jsobj):
    #     """Read a JSON object, and set attributes to match."""
    #     try:
    #         weight, choices = parse_internal_ballot(jsobj)
    #     except ValueError:
    #         # Can happen with "1 2 abc", for example.
    #         # ValueError: invalid literal for int() with base 10: 'abc'
    #         raise JsonDeserializeError("error parsing: %r" % jsobj)
    #     self.__init__(choices=choices, weight=weight) class JsonCaseTestInstance(JsonableMixin):

    """An RCV test case (input and expected output)."""

    meta_attrs = (Attribute('index'),
                  Attribute('rules'), )
    data_attrs = (Attribute('input', cls=JsonCaseContestInput),
                  Attribute('output', cls=JsonCaseTestOutput), ) class JsonCaseTestOutput(JsonableMixin):

    meta_attrs = ()
    data_attrs = (Attribute('rounds', cls=JsonCaseRoundResult), ) class JsonCaseTestsFile(JsonableMixin):

    """A file of test cases (input and expected output)."""

    meta_attrs = (Attribute('version'),
                  Attribute('rule_set'), )
    data_attrs = (Attribute('test_cases', cls=JsonCaseTestInstance), ) class ContestInput(ReprMixin):

    """
    Attributes:
      ballots_resource: a BallotsResource object.
      candidates: an iterable of the names of all candidates, in numeric
        order of their ballot ID.
      name: contest name.
      seat_count: integer number of winners.
    """

    # We include an underscore at the end of id_ since id() is a built-in.
    # TODO: test defaults -- especially properties of default ballots resource.
    # TODO: instead make seat_count part of the "rules".
    def __init__(self, name=None, notes=None, candidates=None, seat_count=None,
                 ballots_resource=None):
        if ballots_resource is None:
            ballots_resource = streams.NullStreamResource()
        if candidates is None:
            candidates = []
        if seat_count is None:
            seat_count = 1

        self.ballots_resource = ballots_resource
        self.candidates = candidates
        self.name = name
        self.notes = notes
        self.seat_count = seat_count

    def repr_info(self):
        return "name=%r" % (self.name, )

    def make_candidates_info(self):
        """Return a CandidatesInfo object."""
        return CandidatesInfo(self.candidates)

    # TODO: remove this.
    def get_candidate_numbers(self):
        """Return an iterable of the candidate numbers."""
        return make_candidate_numbers(len(self.candidates))

    @property
    def should_normalize_ballots(self):
        # Default to normalizing.
        return (self.normalize_ballots is None) or self.normalize_ballots
from contextlib import contextmanager
from random import choice
from openrcv import contestgen, counting, jcmodels, jsonlib, models, utils
from openrcv.formats import jscase
from openrcv.jcmodels import (JsonCaseContestInput, JsonCaseTestInstance,
                              JsonCaseTestOutput, JsonCaseTestsFile)
from openrcv.models import ContestInput
import datetime
import logging
import os
import os.path
import openrcv
#
# Copyright (c) 2014 Chris Jerdonek. All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#

"""Support for managing test cases in the open-rcv-tests repo."""




PERM_ID_CHARS = "0123456789abcdef"


log = logging.getLogger(__name__)


def generate_id(ids):
    """
    Arguments:
      ids: a set of the current IDs.
    """
    while True:
        id_ = "".join(choice(PERM_ID_CHARS) for i in range(8))
        if id_ not in ids:
            break
    return id_


def add_contest_to_contests_file(contest, contests_path):
    """
    Arguments:
      contests_path: a path to a JSON contests file.
    """
    jc_contest = jscase.JsonCaseContestInput.from_model(contest)
    jsobj_contest = jc_contest.to_jsobj()
    data = jsonlib.read_json_path(contests_path)
    data["contests"].append(jsobj_contest)
    jsonlib.write_json(data, path=contests_path)


def _get_jc_contests_file(contests_path):
    js_contests_file = jsonlib.read_json_path(contests_path)
    jc_contests_file = jcmodels.JsonCaseContestsFile.from_jsobj(js_contests_file)
    return jc_contests_file


def _get_tests_file_path(tests_dir, rule_set):
    return os.path.join(tests_dir, "{0}.json".format(rule_set))


def _get_jc_tests_file(tests_dir, rule_set):
    tests_path = _get_tests_file_path(tests_dir, rule_set)
    js_tests_file = jsonlib.read_json_path(tests_path)
    jc_tests_file = JsonCaseTestsFile.from_jsobj(js_tests_file)
    return tests_path, jc_tests_file


def _get_or_make_jc_tests_file_(tests_dir, rule_set):
    tests_path = os.path.join(tests_dir, "{0}.json".format(rule_set))
    try:
        jc_tests_file = _get_jc_tests_file(tests_dir, rule_set)
    except FileNotFoundError:
        jc_tests_file = JsonCaseTestsFile()
    return tests_path, jc_tests_file


# TODO: log normalization conversions (e.g. if they are unequal), and use
#   an equality check on the JSON object to know if there was a difference.
# TODO: normalize the candidate names.
def normalize_contests_file(contests_path):
    jc_file = _get_jc_contests_file(contests_path)
    jc_file.version = openrcv.__version__

    # Show two more than the hard-coded set to show the pattern.
    name_count = len(contestgen.CANDIDATE_NAMES) + 2
    names = contestgen.make_standard_candidate_names(name_count)
    jc_file.candidate_names = names

    jc_contests = jc_file.contests
    ids = set()
    for id_ in (c.id.lower() for c in jc_contests if c.id):
        if id_ in ids:
            raise Exception("duplicate id: {0} (lower-cased)".format(id_))
        ids.add(id_)
    for index, jc_contest in enumerate(jc_contests, start=1):
        jc_contest.index = index
        if not jc_contest.id:
            jc_contest.id = generate_id(ids)
        if jc_contest.normalize_ballots:
            contest = jc_contest.to_model()
            contest.ballots_resource.normalize()

            normalized = JsonCaseContestInput.from_model(contest)