<commit_before># -*- coding: utf-8 -*-


# OpenFisca -- A versatile microsimulation software
# By: OpenFisca Team <<EMAIL>>
#
# Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team
# https://github.com/openfisca
#
# This file is part of OpenFisca.
#
# OpenFisca is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# OpenFisca is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import collections
import copy
import warnings

from . import formulas, legislations, periods, taxbenefitsystems


class AbstractReform(taxbenefitsystems.AbstractTaxBenefitSystem):
    """A reform is a variant of a TaxBenefitSystem, that refers to the real TaxBenefitSystem as its reference."""
    CURRENCY = None
    DECOMP_DIR = None
    DEFAULT_DECOMP_FILE = None
    name = None

    def __init__(self):
        assert self.name is not None
        assert self.reference is not None, 'Reform requires a reference tax-benefit-system.'
        assert isinstance(self.reference, taxbenefitsystems.AbstractTaxBenefitSystem)
        self.Scenario = self.reference.Scenario

        if self.CURRENCY is None:
            currency = getattr(self.reference, 'CURRENCY', None)
            if currency is not None:
                self.CURRENCY = currency
        if self.DECOMP_DIR is None:
            decomp_dir = getattr(self.reference, 'DECOMP_DIR', None)
            if decomp_dir is not None:
                self.DECOMP_DIR = decomp_dir
        if self.DEFAULT_DECOMP_FILE is None:
            default_decomp_file = getattr(self.reference, 'DEFAULT_DECOMP_FILE', None)
            if default_decomp_file is not None:
                self.DEFAULT_DECOMP_FILE = default_decomp_file
        super(AbstractReform, self).__init__(
            entity_class_by_key_plural = self.entity_class_by_key_plural or self.reference.entity_class_by_key_plural,
            legislation_json = self.reference.legislation_json,
            )

    @property
    def full_name(self):
        name = self.name
        if self.reference is not None:
            reference_name = getattr(self.reference, 'name', 'base')
            name = u'.'.join([reference_name, name])
        return name

    def modify_legislation_json(self, modifier_function):
        """
        Copy the reference TaxBenefitSystem legislation_json attribute and return it.
        Used by reforms which need to modify the legislation_json, usually in the build_reform() function.
        Also store it in the cache of the reference TaxBenefitSystem (and not in the base TaxBenefitSystem
        since reforms can be chained).
        """
        full_name = self.full_name
        base_tax_benefit_system = self.base_tax_benefit_system
        reform_legislation_json = base_tax_benefit_system.legislation_json_by_reform_name_cache.get(full_name)
        if reform_legislation_json is None:
            reference_legislation_json = self.reference.legislation_json
            reference_legislation_json_copy = copy.deepcopy(reference_legislation_json)
            reform_legislation_json = modifier_function(reference_legislation_json_copy)
            assert reform_legislation_json is not None, \
                'modifier_function {} in module {} must return the modified legislation_json'.format(
                    modifier_function.__name__,
                    modifier_function.__module__,
                    )
            reform_legislation_json, error = legislations.validate_legislation_json(reform_legislation_json)
            assert error is None, \
                u'The modified legislation_json of the reform "{}" is invalid, error: {}, legislation_json: {}'.format(
                    self.name, error, reform_legislation_json).encode('utf-8')
            base_tax_benefit_system.legislation_json_by_reform_name_cache[full_name] = reform_legislation_json
        self.legislation_json = reform_legislation_json


def clone_entity_class(entity_class):
    class ReformEntity(entity_class):
        pass
    ReformEntity.column_by_name = entity_class.column_by_name.copy()
    return ReformEntity


def compose_reforms(build_reform_list, base_tax_benefit_system):
    """
    Compose reforms: the first reform is built with the given base tax-benefit system,
    then each one is built with the previous one as the reference.
    """
    assert isinstance(build_reform_list, list)
    composed_reform = build_reform_list[0](base_tax_benefit_system)
    for build_reform in build_reform_list[1:]:
        composed_reform = build_reform(composed_reform)
    return composed_reform


def make_reform(name, reference, decomposition_dir_name = None, decomposition_file_name = None, new_formulas = None):
    """
    Return a Reform class inherited from AbstractReform.

    new_formula is deprecated.
    """
    assert isinstance(name, basestring)
    assert isinstance(reference, taxbenefitsystems.AbstractTaxBenefitSystem)
    reform_entity_class_by_key_plural = {
        key_plural: clone_entity_class(entity_class)
        for key_plural, entity_class in reference.entity_class_by_key_plural.iteritems()
        }
    reform_entity_class_by_symbol = {
        entity_class.symbol: entity_class
        for entity_class in reform_entity_class_by_key_plural.itervalues()
        }
    reform_name = name
    reform_reference = reference

    class Reform(AbstractReform):
        DECOMP_DIR = decomposition_dir_name
        DEFAULT_DECOMP_FILE = decomposition_file_name
        entity_class_by_key_plural = reform_entity_class_by_key_plural
        name = reform_name
        reference = reform_reference

        formula = staticmethod(formulas.make_reference_formula_decorator(
            entity_class_by_symbol = reform_entity_class_by_symbol,
            update = True,
            ))

        @classmethod
        def input_variable(cls, entity_class = None, **kwargs):
            # Ensure that entity_class belongs to reform (instead of reference tax-benefit system).
            entity_class = cls.entity_class_by_key_plural[entity_class.key_plural]
            assert 'update' not in kwargs
            kwargs['update'] = True
            return formulas.reference_input_variable(entity_class = entity_class, **kwargs)

    if new_formulas is not None:
        warnings.warn(
            "new_formulas is deprecated. Use reform.formula decorator instead on the formula classes, "
            "reform being the object returned by make_reform",
            DeprecationWarning,
            )
        assert isinstance(new_formulas, collections.Sequence)
        for new_formula in new_formulas:
            Reform.formula(new_formula)

    return Reform


# Legislation helpers


def update_legislation(legislation_json, path, period = None, value = None, start = None, stop = None):
    """<commit_msg>Check that reform has not been constructed when a formula or an input_variable is added<commit_after># -*- coding: utf-8 -*-


# OpenFisca -- A versatile microsimulation software
# By: OpenFisca Team <<EMAIL>>
#
# Copyright (C) 2011, 2012, 2013, 2014, 2015 OpenFisca Team
# https://github.com/openfisca
#
# This file is part of OpenFisca.
#
# OpenFisca is free software; you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# OpenFisca is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.


import collections
import copy
import warnings

from . import formulas, legislations, periods, taxbenefitsystems


class AbstractReform(taxbenefitsystems.AbstractTaxBenefitSystem):
    """A reform is a variant of a TaxBenefitSystem, that refers to the real TaxBenefitSystem as its reference."""
    CURRENCY = None
    DECOMP_DIR = None
    DEFAULT_DECOMP_FILE = None
    name = None

    def __init__(self):
        assert self.name is not None
        assert self.reference is not None, 'Reform requires a reference tax-benefit-system.'
        assert isinstance(self.reference, taxbenefitsystems.AbstractTaxBenefitSystem)
        self.Scenario = self.reference.Scenario

        if self.CURRENCY is None:
            currency = getattr(self.reference, 'CURRENCY', None)
            if currency is not None:
                self.CURRENCY = currency
        if self.DECOMP_DIR is None:
            decomp_dir = getattr(self.reference, 'DECOMP_DIR', None)
            if decomp_dir is not None:
                self.DECOMP_DIR = decomp_dir
        if self.DEFAULT_DECOMP_FILE is None:
            default_decomp_file = getattr(self.reference, 'DEFAULT_DECOMP_FILE', None)
            if default_decomp_file is not None:
                self.DEFAULT_DECOMP_FILE = default_decomp_file
        super(AbstractReform, self).__init__(
            entity_class_by_key_plural = self.entity_class_by_key_plural or self.reference.entity_class_by_key_plural,
            legislation_json = self.reference.legislation_json,
            )

    @property
    def full_name(self):
        name = self.name
        if self.reference is not None:
            reference_name = getattr(self.reference, 'name', 'base')
            name = u'.'.join([reference_name, name])
        return name

    def modify_legislation_json(self, modifier_function):
        """
        Copy the reference TaxBenefitSystem legislation_json attribute and return it.
        Used by reforms which need to modify the legislation_json, usually in the build_reform() function.
        Also store it in the cache of the reference TaxBenefitSystem (and not in the base TaxBenefitSystem
        since reforms can be chained).
        """
        full_name = self.full_name
        base_tax_benefit_system = self.base_tax_benefit_system
        reform_legislation_json = base_tax_benefit_system.legislation_json_by_reform_name_cache.get(full_name)
        if reform_legislation_json is None:
            reference_legislation_json = self.reference.legislation_json
            reference_legislation_json_copy = copy.deepcopy(reference_legislation_json)
            reform_legislation_json = modifier_function(reference_legislation_json_copy)
            assert reform_legislation_json is not None, \
                'modifier_function {} in module {} must return the modified legislation_json'.format(
                    modifier_function.__name__,
                    modifier_function.__module__,
                    )
            reform_legislation_json, error = legislations.validate_legislation_json(reform_legislation_json)
            assert error is None, \
                u'The modified legislation_json of the reform "{}" is invalid, error: {}, legislation_json: {}'.format(
                    self.name, error, reform_legislation_json).encode('utf-8')
            base_tax_benefit_system.legislation_json_by_reform_name_cache[full_name] = reform_legislation_json
        self.legislation_json = reform_legislation_json


def clone_entity_class(entity_class):
    class ReformEntity(entity_class):
        pass
    ReformEntity.column_by_name = entity_class.column_by_name.copy()
    return ReformEntity


def compose_reforms(build_reform_list, base_tax_benefit_system):
    """
    Compose reforms: the first reform is built with the given base tax-benefit system,
    then each one is built with the previous one as the reference.
    """
    assert isinstance(build_reform_list, list)
    composed_reform = build_reform_list[0](base_tax_benefit_system)
    for build_reform in build_reform_list[1:]:
        composed_reform = build_reform(composed_reform)
    return composed_reform


def make_reform(name, reference, decomposition_dir_name = None, decomposition_file_name = None, new_formulas = None):
    """
    Return a Reform class inherited from AbstractReform.

    new_formula is deprecated.
    """
    assert isinstance(name, basestring)
    assert isinstance(reference, taxbenefitsystems.AbstractTaxBenefitSystem)
    reform_entity_class_by_key_plural = {
        key_plural: clone_entity_class(entity_class)
        for key_plural, entity_class in reference.entity_class_by_key_plural.iteritems()
        }
    reform_entity_class_by_symbol = {
        entity_class.symbol: entity_class
        for entity_class in reform_entity_class_by_key_plural.itervalues()
        }
    reform_name = name
    reform_reference = reference

    class Reform(AbstractReform):
        _constructed = False
        DECOMP_DIR = decomposition_dir_name
        DEFAULT_DECOMP_FILE = decomposition_file_name
        entity_class_by_key_plural = reform_entity_class_by_key_plural
        name = reform_name
        reference = reform_reference

        def __init__(self):
            super(Reform, self).__init__()
            Reform._constructed = True

        @classmethod
        def formula(cls, column):
            assert not cls._constructed, \
                'You are trying to add a formula to a Reform but its constructor has already been called.'
            return formulas.make_reference_formula_decorator(
                entity_class_by_symbol = reform_entity_class_by_symbol,
                update = True,
                )(column)

        @classmethod
        def input_variable(cls, entity_class = None, **kwargs):
            assert not cls._constructed, \
                'You are trying to add an input variable to a Reform but its constructor has already been called.'
            # Ensure that entity_class belongs to reform (instead of reference tax-benefit system).
            entity_class = cls.entity_class_by_key_plural[entity_class.key_plural]
            assert 'update' not in kwargs
            kwargs['update'] = True
            return formulas.reference_input_variable(entity_class = entity_class, **kwargs)

    if new_formulas is not None:
        warnings.warn(
            "new_formulas is deprecated. Use reform.formula decorator instead on the formula classes, "
            "reform being the object returned by make_reform",
            DeprecationWarning,
            )
        assert isinstance(new_formulas, collections.Sequence)
        for new_formula in new_formulas:
            Reform.formula(new_formula)

    return Reform


# Legislation helpers


def update_legislation(legislation_json, path, period = None, value = None, start = None, stop = None):
    """
{'fraction_non_alphanumeric': 0.06291216037984701, 'fraction_numerical': 0.0032972830387760486, 'mean_word_length': 3.026818906001062, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 9, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5492707', 'n_tokens_mistral': 4350, 'n_tokens_neox': 4183, 'n_words': 1176}