Skip to content

Commit

Permalink
move new context decorator to context module
Browse files Browse the repository at this point in the history
  • Loading branch information
mscarey committed Jun 9, 2019
1 parent 8f86422 commit dae8509
Show file tree
Hide file tree
Showing 6 changed files with 138 additions and 107 deletions.
67 changes: 67 additions & 0 deletions authorityspoke/context.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""
Decorators for memoizing generic :class:`.Factor`\s.
Used when changing an abstract :class:`.Rule` from
one concrete context to another.
"""

from __future__ import annotations

import functools
Expand All @@ -7,8 +14,68 @@
from typing import Optional, Sequence, Tuple, Union


def new_context_helper(func: Callable):
"""
Search :class:`.Factor` for generic :class:`.Factor`\s to use in new context.
Decorator for :meth:`Factor.new_context`.
If a :class:`list` has been passed in rather than a :class:`dict`, uses
the input as a series of :class:`Factor`\s to replace the
:attr:`~Factor.generic_factors` from the calling object.
Also, if ``changes`` contains a replacement for the calling
object, the decorator returns the replacement and never calls
the decorated function.
:param factor:
a :class:`.Factor` that is having its generic :class:`.Factor`\s
replaced to change context (for instance, to change to the context
of a different case involving parties represented by different
:class:`.Entity` objects).
:param changes:
indicates the which generic :class:`.Factor`\s within ``factor`` should
be replaced and what they should be replaced with.
:returns:
a new :class:`.Factor` object in the new context.
"""

@functools.wraps(func)
def wrapper(
factor: Factor, changes: Optional[Union[Sequence[Factor], Dict[Factor, Factor]]]
) -> Factor:

if changes is not None:
if not isinstance(changes, Iterable):
changes = (changes,)
if not isinstance(changes, dict):
generic_factors = factor.generic_factors
if len(generic_factors) != len(changes):
raise ValueError(
'If the parameter "changes" is not a list of '
+ "replacements for every element of factor.generic_factors, "
+ 'then "changes" must be a dict where each key is a Factor '
+ "to be replaced and each value is the corresponding "
+ "replacement Factor."
)
changes = dict(zip(generic_factors, changes))
for context_factor in changes:
if factor.name == context_factor or (
factor.means(context_factor) and factor.name == context_factor.name
):
return changes[context_factor]

return func(factor, changes)

return wrapper


def log_mentioned_context(func: Callable):
"""
Retrieve cached :class:`.Factor` instead of building one with the decorated method.
Decorator for :meth:`.Factor.from_dict()` and :meth:`.Enactment.from_dict()`.
If factor_record is a :class:`str` instead of a :class:`dict`, looks up the
Expand Down
65 changes: 16 additions & 49 deletions authorityspoke/factors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,58 +13,10 @@

from dataclasses import astuple, dataclass

from authorityspoke.context import log_mentioned_context
from authorityspoke.context import log_mentioned_context, new_context_helper
from authorityspoke.predicates import Predicate
from authorityspoke.relations import Relation


def new_context_helper(func: Callable):
"""
Decorator for :meth:`Factor.new_context`.
If a :class:`list` has been passed in rather than a :class:`dict`, uses
the input as a series of :class:`Factor`\s to replace the
:attr:`~Factor.generic_factors` from the calling object.
Also, if ``context`` contains a replacement for the calling
object, the decorator returns the replacement and never calls
the decorated function.
"""

@functools.wraps(func)
def wrapper(
factor: Factor, context: Optional[Union[Sequence[Factor], Dict[Factor, Factor]]]
) -> Factor:

if context is not None:
if not isinstance(context, Iterable):
context = (context,)
if any(not isinstance(item, (Factor, str)) for item in context):
raise TypeError(
'Each item in "context" must be a Factor or the name of a Factor'
)
if not isinstance(context, dict):
generic_factors = factor.generic_factors
if len(generic_factors) != len(context):
raise ValueError(
'If the parameter "changes" is not a list of '
+ "replacements for every element of factor.generic_factors, "
+ 'then "changes" must be a dict where each key is a Factor '
+ "to be replaced and each value is the corresponding "
+ "replacement Factor."
)
context = dict(zip(generic_factors, context))
for context_factor in context:
if factor.name == context_factor or (
factor.means(context_factor) and factor.name == context_factor.name
):
return context[context_factor]

return func(factor, context)

return wrapper


@dataclass(frozen=True)
class Factor(ABC):
"""
Expand Down Expand Up @@ -467,6 +419,10 @@ def new_context(self, changes: Dict[Factor, Factor]) -> Factor:
:returns:
a new :class:`.Factor` object with the replacements made.
"""
if any(not isinstance(item, (str, Factor)) for item in changes):
raise TypeError(
'Each item in "changes" must be a Factor or the name of a Factor'
)
new_dict = self.__dict__.copy()
for name in self.context_factor_names:
new_dict[name] = self.__dict__[name].new_context(changes)
Expand Down Expand Up @@ -1076,7 +1032,16 @@ def __str__(self):


def means(left: Factor, right: Factor) -> bool:
"""
Call :meth:`.Factor.means` as function alias
This only exists because :class:`.Relation` objects expect
a function rather than a method for :attr:`~.Relation.comparison`.
:returns:
whether ``other`` is another :class:`Factor` with the same
meaning as ``self``.
"""
return left.means(right)


Expand Down Expand Up @@ -1124,6 +1089,8 @@ class Entity(Factor):

def means(self, other):
"""
Test whether ``other`` has the same meaning as ``self``.
``generic`` :class:`Entity` objects are considered equivalent
in meaning as long as they're the same class. If not ``generic``,
they're considered equivalent if all their attributes are the same.
Expand Down
13 changes: 10 additions & 3 deletions authorityspoke/opinions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
"""
:class:`Court` documents that decide litigation and posit :class:`.Rule`\s.
Unlike most other ``authorityspoke`` classes, :class:`Opinion`\s are not frozen.
"""

from __future__ import annotations

from typing import Any, Dict, List, Tuple
Expand Down Expand Up @@ -214,7 +220,10 @@ def opinions_from_response(results, to_dict):

def contradicts(self, other: Union[Opinion, Rule]) -> bool:
"""
Test whether ``other`` is or contains a :class:`.Rule` contradicted by ``self``.
:param other:
another :class:`.Opinion` or :class:`.Rule`
:returns:
a bool indicating whether any holding of ``self`` is
Expand Down Expand Up @@ -243,9 +252,7 @@ def from_dict(
cls, decision_dict: Dict[str, Any], lead_only: bool = True
) -> Union[Opinion, Iterator[Opinion]]:
"""
Creates and returns one or more :class:`.Opinion` objects
from a :py:class:`dict` derived from an opinion record from the
`Caselaw Access Project API <https://api.case.law/v1/cases/>`_.
Create and return one or more :class:`.Opinion` objects.
:param decision_dict:
A record of an opinion loaded from JSON from the
Expand Down
68 changes: 40 additions & 28 deletions authorityspoke/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ def from_json(
cls,
filename: str,
directory: Optional[pathlib.Path] = None,
regime: Optional["Regime"] = None,
regime: Optional[Regime] = None,
) -> List[Rule]:
"""
Load a list of ``Rule``\s from JSON.
Expand Down Expand Up @@ -602,8 +602,9 @@ def get_mentioned_factors(

@dataclass(frozen=True)
class ProceduralRule(Rule):

"""
A statement of a legal doctrine about a :class:`.Procedure` for litigation.
:param procedure:
a :class:`.Procedure` containing the inputs, and despite
:class:`.Factor`\s and resulting outputs when this rule
Expand Down Expand Up @@ -679,8 +680,9 @@ def __post_init__(self):
def from_dict(
cls, record: Dict, mentioned: List[Factor], regime: Optional["Regime"] = None
) -> Tuple[ProceduralRule, List[Factor]]:

"""
Make :class:`Rule` from a :py:class:`dict` of strings and a list of mentioned :class:`.Factor`\s.
:param record:
a :class:`dict` derived from the JSON format that
lists ``mentioned_entities`` followed by a
Expand Down Expand Up @@ -748,6 +750,8 @@ def list_from_records(
@property
def context_factors(self) -> Tuple:
"""
Call :class:`Procedure`\'s :meth:`~Procedure.context_factors` method.
:returns:
context_factors from ``self``'s :class:`Procedure`
"""
Expand All @@ -756,6 +760,8 @@ def context_factors(self) -> Tuple:
@property
def despite(self):
"""
Call :class:`Procedure`\'s :meth:`~Procedure.despite` method.
:returns:
despite :class:`.Factors` from ``self``'s :class:`Procedure`
"""
Expand All @@ -764,7 +770,7 @@ def despite(self):
@property
def generic_factors(self) -> List[Optional[Factor]]:
"""
:class:`.Factor`\s that can be replaced without changing ``self``\s meaning.
Get :class:`.Factor`\s that can be replaced without changing ``self``\s meaning.
:returns:
generic :class:`.Factors` from ``self``'s :class:`Procedure`
Expand Down Expand Up @@ -831,6 +837,8 @@ def contradicts(self, other) -> bool:

def _contradicts_if_valid(self, other) -> bool:
"""
Test if ``self`` contradicts ``other``, assuming ``rule_valid`` and ``decided``.
:returns:
whether ``self`` contradicts ``other``,
assuming that ``rule_valid`` and ``decided`` are
Expand Down Expand Up @@ -858,7 +866,9 @@ def _contradicts_if_valid(self, other) -> bool:

def __ge__(self, other) -> bool:
"""
Implication method. See :meth:`.Procedure.implies_all_to_all`
Test for implication.
See :meth:`.Procedure.implies_all_to_all`
and :meth:`.Procedure.implies_all_to_some` for
explanations of how ``inputs``, ``outputs``,
and ``despite`` :class:`.Factor`\s affect implication.
Expand Down Expand Up @@ -897,10 +907,11 @@ def __ge__(self, other) -> bool:
return False

def _implies_if_decided(self, other) -> bool:

"""
Simplified version of the :meth:`ProceduralRule.__ge__`
implication function.
Test if ``self`` implies ``other`` if they're both decided.
This is a partial version of the
:meth:`ProceduralRule.__ge__` implication function.
:returns:
whether ``self`` implies ``other``, assuming that
Expand All @@ -924,12 +935,15 @@ def _implies_if_decided(self, other) -> bool:

def _implies_if_valid(self, other) -> bool:
"""
Partial version of the :meth:`ProceduralRule.__ge__`
implication function.
Test if ``self`` implies ``other`` if they're valid and decided.
This is a partial version of the
:meth:`ProceduralRule.__ge__` implication function.
:returns: whether ``self`` implies ``other``, assuming that
both are :class:`ProceduralRule`\s,, and
``rule_valid`` and ``decided`` are ``True`` for both of them.
:returns:
whether ``self`` implies ``other``, assuming that
both are :class:`ProceduralRule`\s, and
``rule_valid`` and ``decided`` are ``True`` for both of them.
"""

if not all(
Expand Down Expand Up @@ -959,16 +973,19 @@ def _implies_if_valid(self, other) -> bool:

def __len__(self):
"""
Count generic :class:`.Factor`\s needed as context for this :class:`Rule`.
:returns:
the number of generic :class:`.Factor`\s needed to provide
context for this :class:`Rule`, which currently is just the
generic :class:`.Factor`\s needed for the ``procedure``.
the number of generic :class:`.Factor`\s needed for
self's :class:`.Procedure`.
"""

return len(self.procedure)

def means(self, other: ProceduralRule) -> bool:
"""
Test whether ``other`` has the same meaning as ``self``.
:returns:
whether ``other`` is a :class:`ProceduralRule` with the
same meaning as ``self``.
Expand All @@ -988,15 +1005,8 @@ def means(self, other: ProceduralRule) -> bool:
)

def negated(self):
return ProceduralRule(
procedure=self.procedure,
enactments=self.enactments,
enactments_despite=self.enactments_despite,
mandatory=self.mandatory,
universal=self.universal,
rule_valid=not self.rule_valid,
decided=self.decided,
)
"""Get new copy of ``self`` with an opposite value for ``rule_valid``."""
return self.evolve("rule_valid")

def __str__(self):
def factor_catalog(factors: List[Union[Factor, Enactment]], tag: str) -> str:
Expand All @@ -1018,9 +1028,11 @@ def factor_catalog(factors: List[Union[Factor, Enactment]], tag: str) -> str:

class Attribution:
"""
An assertion about the meaning of a prior Opinion. Either a user or an Opinion
may make an Attribution to an Opinion. An Attribution may attribute either
a Rule or a further Attribution.
An assertion about the meaning of a prior :class:`.Opinion`.
Either a user or an :class:`.Opinion` may make an Attribution
to an :class:`.Opinion`. An Attribution may attribute either a
:class:`.Rule` or a further Attribution.
"""

pass

0 comments on commit dae8509

Please sign in to comment.