-
Notifications
You must be signed in to change notification settings - Fork 2
/
holdings.py
550 lines (464 loc) · 20.3 KB
/
holdings.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
r"""
:class:`Holding`\s describe :class:`.Opinion`\s` attitudes toward :class:`.Rule`\s.
:class:`Holding`\s are text passages within :class:`.Opinion`\s
in which :class:`.Court` posits, or rejects, the validity of a
:class:`.Rule` within the :class:`.Court`\'s :class:`.Jurisdiction`,
or the :class:`.Court` asserts that the validity of the :class:`.Rule`
should be considered undecided.
"""
from __future__ import annotations
from itertools import chain
from typing import Any, Dict, Iterator, List
from typing import Optional, Sequence, Tuple, Union
from dataclasses import dataclass
from authorityspoke.comparisons import Comparable
from authorityspoke.explanations import Explanation
from authorityspoke.factors import ContextRegister, Factor, new_context_helper
from authorityspoke.groups import ComparableGroup
from authorityspoke.formatting import indented, wrapped
from authorityspoke.procedures import Procedure
from authorityspoke.rules import Rule
@dataclass(frozen=True)
class Holding(Factor):
"""
An :class:`.Opinion`\'s announcement that it posits or rejects a legal :class:`.Rule`.
Note that if an opinion merely says the court is not deciding whether
a :class:`.Rule` is valid, there is no :class:`Holding`, and no
:class:`.Rule` object should be created. Deciding not to decide
a :class:`Rule`\'s validity is not the same thing as deciding
that the :class:`.Rule` is undecided.
:param rule:
a statement of a legal doctrine about a :class:`.Procedure` for litigation.
:param rule_valid:
``True`` means the :class:`.Rule` is asserted to be valid (or
useable by a court in litigation). ``False`` means it's asserted
to be invalid.
:param decided:
``False`` means that it should be deemed undecided
whether the :class:`.Rule` is valid, and thus can have the
effect of overruling prior holdings finding the :class:`.Rule`
to be either valid or invalid. Seemingly, ``decided=False``
should render the ``rule_valid`` flag irrelevant.
:param exclusive:
if True, the stated rule is asserted to be the only way to establish
the output that is the output of the rule.
:param generic:
if True, indicates that the specific attributes of this holding
are irrelevant in the context of a different holding that is
referencing this holding.
"""
rule: Rule
rule_valid: bool = True
decided: bool = True
exclusive: bool = False
generic: bool = False
def __post_init__(self):
if self.exclusive:
if not self.rule_valid:
raise NotImplementedError(
"The ability to state that it is not 'valid' to assert "
+ "that a Rule is the 'exclusive' way to reach an output is "
+ "not implemented, so 'rule_valid' cannot be False while "
+ "'exclusive' is True. Try expressing this in another way "
+ "without the 'exclusive' keyword."
)
if not self.decided:
raise NotImplementedError(
"The ability to state that it is not 'decided' whether "
+ "a Rule is the 'exclusive' way to reach an output is "
+ "not implemented. Try expressing this in another way "
+ "without the 'exclusive' keyword."
)
@property
def procedure(self):
"""Get Procedure from Rule."""
return self.rule.procedure
@property
def despite(self):
"""Get Factors that specifically don't preclude application of the Holding."""
return self.rule.procedure.despite
@property
def inputs(self):
"""Get inputs from Procedure."""
return self.rule.procedure.inputs
@property
def outputs(self):
"""Get outputs from Procedure."""
return self.rule.procedure.outputs
@property
def enactments(self):
"""Get Enactments required to apply the Holding."""
return self.rule.enactments
@property
def enactments_despite(self):
"""Get Enactments that specifically don't preclude application of the Holding."""
return self.rule.enactments_despite
@property
def context_factors(self) -> Tuple:
r"""
Call :class:`Procedure`\'s :meth:`~Procedure.context_factors` method.
:returns:
context_factors from ``self``'s :class:`Procedure`
"""
return self.rule.procedure.context_factors
@property
def generic_factors(self) -> List[Factor]:
r"""
Get :class:`.Factor`\s that can be replaced without changing ``self``\s meaning.
:returns:
generic :class:`.Factor`\s from ``self``'s :class:`Procedure`
"""
return self.rule.generic_factors
@property
def mandatory(self) -> bool:
"""Whether court "MUST" apply holding when it is applicable."""
return self.rule.mandatory
@property
def universal(self) -> bool:
"""Whether holding is applicable in "ALL" cases where inputs are present."""
return self.rule.universal
def add_if_not_exclusive(self, other: Holding) -> Optional[Holding]:
"""Show how first Holding triggers second, assumed not to be "exclusive" way to reach result."""
new_rule = self.rule + other.rule
if new_rule is None:
return None
return self.evolve({"rule": self.rule + other.rule})
def add_holding(self, other: Holding) -> Optional[Holding]:
"""Show how first Holding triggers the second."""
if not (self.decided and other.decided):
raise NotImplementedError(
"Adding is not implemented for Holdings that assert a Rule is not decided."
)
if not (self.rule_valid and other.rule_valid):
raise NotImplementedError(
"Adding is not implemented for Holdings that assert a Rule is not valid."
)
for self_holding in self.nonexclusive_holdings:
for other_holding in other.nonexclusive_holdings:
added = self_holding.add_if_not_exclusive(other_holding)
if added is not None:
return added
return None
def __add__(self, other: Factor) -> Optional[Union[Rule, Holding]]:
if isinstance(other, Rule):
other = Holding(rule=other)
if isinstance(other, Holding):
return self.add_holding(other)
return self.evolve({"rule": self.rule + other})
def _explanations_contradiction_of_holding(
self, other: Holding, context: ContextRegister
) -> Iterator[Explanation]:
for self_holding in self.nonexclusive_holdings:
for other_holding in other.nonexclusive_holdings:
for register in self_holding._contradicts_if_not_exclusive(
other_holding, context=context
):
yield Explanation(
needs_match=self_holding,
available=other_holding,
context=register,
relation="CONTRADICTION",
)
def explanations_contradiction(
self, other: Factor, context: ContextRegister = None
) -> Iterator[Explanation]:
r"""
Find context matches that would result in a contradiction with other.
Works by testing whether ``self`` would imply ``other`` if
``other`` had an opposite value for ``rule_valid``.
This method takes three main paths depending on
whether the holdings ``self`` and ``other`` assert that
rules are decided or undecided.
A ``decided`` :class:`Rule` can never contradict
a previous statement that any :class:`Rule` was undecided.
If rule A implies rule B, then a holding that B is undecided
contradicts a prior :class:`Rule` deciding that
rule A is valid or invalid.
:param other:
The :class:`.Factor` to be compared to self. Unlike with
:meth:`~Holding.contradicts`\, this method cannot be called
with an :class:`.Opinion` for `other`.
:returns:
a generator yielding :class:`.ContextRegister`\s that cause a
contradiction.
"""
if context is None:
context = ContextRegister()
if isinstance(other, Procedure):
other = Rule(procedure=other)
if isinstance(other, Rule):
other = Holding(rule=other)
if isinstance(other, self.__class__):
yield from self._explanations_contradiction_of_holding(other, context)
elif isinstance(other, Factor):
yield from [] # no possible contradiction
elif hasattr(other, "explanations_contradiction"):
if context:
context = context.reversed()
yield from other.explanations_contradiction(self, context=context)
else:
raise TypeError(
f"'Contradicts' test not implemented for types "
f"{self.__class__} and {other.__class__}."
)
def _contradicts_if_not_exclusive(
self, other: Holding, context: ContextRegister = None
) -> Iterator[ContextRegister]:
if context is None:
context = ContextRegister()
if isinstance(other, Holding) and other.decided:
if self.decided:
yield from self._explanations_implies_if_not_exclusive(
other.negated(), context=context
)
else:
yield from chain(
other._implies_if_decided(self),
other._implies_if_decided(self.negated()),
)
def _explanations_implies_if_not_exclusive(
self, other: Factor, context: ContextRegister = None
) -> Iterator[ContextRegister]:
if not isinstance(other, self.__class__):
raise TypeError
if self.decided and other.decided:
yield from self._implies_if_decided(other, context)
# A holding being undecided doesn't seem to imply that
# any other holding is undecided, except itself and the
# negation of itself.
# It doesn't seem that any holding being undecided implies
# that any holding is decided, or vice versa.
elif not self.decided and not other.decided:
yield from chain(
self.explanations_same_meaning(other, context),
self.explanations_same_meaning(other.negated(), context),
)
def __ge__(self, other: Optional[Factor]) -> bool:
return self.implies(other)
def implies(
self, other: Optional[Comparable], context: ContextRegister = None
) -> bool:
r"""
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.
:param other:
A :class:`Holding` to compare to self, or a :class:`.Rule` to
convert into such a :class:`Holding` and then compare
:returns:
whether ``self`` implies ``other``
"""
if other is None:
return True
if isinstance(other, (Rule, Procedure)):
other = Holding(rule=other)
if not isinstance(other, self.__class__):
if hasattr(other, "implied_by"):
if context:
context = context.reversed()
return other.implied_by(self, context=context)
return False
return any(
explanation is not None
for explanation in self.explanations_implication(other, context)
)
def explanations_implication(
self, other: Holding, context: Optional[ContextRegister] = None
) -> Iterator[ContextRegister]:
"""Yield contexts that would cause self and other to have same meaning."""
if self.exclusive is other.exclusive is False:
yield from self._explanations_implies_if_not_exclusive(
other, context=context
)
else:
yield from self.nonexclusive_holdings.explanations_implication(
other.nonexclusive_holdings, context=context
)
def implied_by(
self, other: Factor, context: Optional[ContextRegister] = None
) -> bool:
r"""
Test if other implies self.
This function is for handling implication checks for classes
that don't know the structure of the :class:`Holding` class,
such as :class:`.Fact` and :class:`.Rule`\.
"""
if context:
context = context.reversed()
if isinstance(other, Rule):
return Holding(rule=other).implies(self, context=context)
return other.implies(self, context=context)
def _implies_if_decided(
self, other: Holding, context: Optional[ContextRegister] = None
) -> Iterator[ContextRegister]:
r"""
Test if ``self`` implies ``other`` if they're both decided.
This is a partial version of the
:meth:`Holding.__ge__` implication function.
:returns:
whether ``self`` implies ``other``, assuming that
``self.decided == other.decided == True`` and that
``self`` and ``other`` are both :class:`Holding`\s,
although ``rule_valid`` can be ``False``.
"""
if self.rule_valid and other.rule_valid:
yield from self.rule.explanations_implication(other.rule, context)
elif not self.rule_valid and not other.rule_valid:
yield from other.rule.explanations_implication(self.rule, context)
# Looking for implication where self.rule_valid != other.rule_valid
# is equivalent to looking for contradiction.
# If decided rule A contradicts B, then B also contradicts A
else:
yield from self.rule.explanations_contradiction(other.rule, context)
def __len__(self):
r"""
Count generic :class:`.Factor`\s needed as context for this :class:`Holding`.
:returns:
the number of generic :class:`.Factor`\s needed for
self's :class:`.Procedure`.
"""
return len(self.rule.procedure)
@property
def inferred_from_exclusive(self) -> List[Holding]:
r"""
Yield :class:`Holding`\s that can be inferred from the "exclusive" flag.
The generator will be empty if `self.exclusive` is False.
"""
if self.exclusive:
return [
Holding(rule=modified_rule)
for modified_rule in self.rule.get_contrapositives()
]
return []
def evolve(self, changes: Union[str, Sequence[str], Dict[str, Any]]) -> Holding:
"""
Make new object with attributes from ``self.__dict__``, replacing attributes as specified.
:param changes:
a :class:`dict` where the keys are names of attributes
of self, and the values are new values for those attributes, or
else an attribute name or :class:`list` of names that need to
have their values replaced with their boolean opposite.
:returns:
a new object initialized with attributes from
``self.__dict__``, except that any attributes named as keys in the
changes parameter are replaced by the corresponding value.
"""
changes = self._make_dict_to_evolve(changes)
changes = self._evolve_attribute(changes, "rule")
new_values = self._evolve_from_dict(changes)
return self.__class__(**new_values)
def explanations_same_meaning(
self, other: Factor, context: Optional[ContextRegister] = None
) -> Iterator[ContextRegister]:
"""Yield contexts that would cause self and other to have same meaning."""
if (
isinstance(other, self.__class__)
and self.rule_valid == other.rule_valid
and self.decided == other.decided
):
yield from self.rule.explanations_same_meaning(other.rule, context)
def negated(self):
"""Get new copy of ``self`` with an opposite value for ``rule_valid``."""
return self.evolve({"rule_valid": not self.rule_valid, "exclusive": False})
@new_context_helper
def new_context(self, changes: ContextRegister) -> Factor:
"""
Create new :class:`Holding`, replacing keys of ``changes`` with values.
:returns:
a version of ``self`` with the new context.
"""
return Holding(
rule=self.rule.new_context(changes),
rule_valid=self.rule_valid,
decided=self.decided,
)
@property
def nonexclusive_holdings(self) -> HoldingGroup:
r"""Yield all :class:`.Holding`\s with `exclusive is False` implied by self."""
if not self.exclusive:
return HoldingGroup([self])
holdings = [self.evolve("exclusive")] + self.inferred_from_exclusive
return HoldingGroup(holdings)
def _union_if_not_exclusive(
self, other: Holding, context: ContextRegister
) -> Optional[Holding]:
if self.decided is other.decided is False:
if self.rule.implies(other.rule, context=context):
return other
if other.rule.implies(self.rule, context=context.reversed()):
return self
return None
if not self.decided or not other.decided:
return None
if self.rule_valid != other.rule_valid:
return None
if self.rule_valid is False:
# If a Rule with input A present is not valid
# and a Rule with input A absent is also not valid
# then a version of the Rule with input A
# omitted is also not valid.
raise NotImplementedError(
"The union operation is not yet implemented for Holdings "
"that assert a Rule is not valid."
)
new_rule = self.rule.union(other.rule, context=context)
if not new_rule:
return None
return self.evolve({"rule": new_rule, "exclusive": False})
def _union_with_holding(
self, other: Holding, context: ContextRegister
) -> Optional[Holding]:
for self_holding in self.nonexclusive_holdings:
for other_holding in other.nonexclusive_holdings:
united = self_holding._union_if_not_exclusive(
other_holding, context=context
)
if united is not None:
return united
return None
def union(
self, other: Union[Rule, Holding], context: Optional[ContextRegister] = None
) -> Optional[Holding]:
"""Infer a Holding from all inputs and outputs of self and other, in context."""
context = context or ContextRegister()
if isinstance(other, Rule):
other = Holding(rule=other)
if not isinstance(other, Holding):
raise TypeError
return self._union_with_holding(other, context=context)
def __or__(self, other: Union[Rule, Holding]) -> Optional[Holding]:
"""Infer a Holding from all inputs and outputs of self and other."""
return self.union(other)
def own_attributes(self) -> Dict[str, Any]:
"""
Return attributes of ``self`` that aren't inherited from another class.
Used for getting parameters to pass to :meth:`~Holding.__init__`
when generating a new object.
"""
attrs = self.__dict__.copy()
for group in Procedure.context_factor_names:
attrs.pop(group, None)
for group in Rule.enactment_attr_names:
attrs.pop(group, None)
attrs.pop("procedure", None)
return attrs
def __str__(self):
action = (
"consider UNDECIDED"
if not self.decided
else ("ACCEPT" if self.rule_valid else "REJECT")
)
exclusive = (
(
f" that the EXCLUSIVE way to reach "
f"{self.rule.outputs[0].short_string} is"
)
if self.exclusive
else ""
)
rule_text = indented(str(self.rule))
text = wrapped(f"the Holding to {action}{exclusive}") + f"\n{rule_text}"
return text
HoldingGroup = ComparableGroup[Holding]