-
Notifications
You must be signed in to change notification settings - Fork 2
/
opinions.py
344 lines (293 loc) · 12.5 KB
/
opinions.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
r"""
: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 Dict, Iterable, List, Tuple
from typing import Optional, Sequence, Union
import datetime
import logging
import re
from dataclasses import dataclass, field
from authorityspoke.factors import Factor, TextLinkDict
from authorityspoke.enactments import Enactment
from authorityspoke.holdings import Holding
from authorityspoke.rules import Rule
from authorityspoke.selectors import TextQuoteSelector
logger = logging.getLogger(__name__)
@dataclass
class Opinion:
"""
A document that resolves legal issues in a case and posits legal holdings.
Usually an opinion must have ``position="majority"``
to create holdings binding on any courts.
:param name:
full name of the opinion, e.g. "ORACLE AMERICA, INC.,
Plaintiff-Appellant, v. GOOGLE INC., Defendant-Cross-Appellant"
:param name_abbreviation:
shorter name of the opinion, e.g. "Oracle America, Inc. v. Google Inc."
:param citations:
citations to the opinion, usually in the format
``[Volume Number] [Reporter Name Abbreviation] [Page Number]``
:param first_page:
the page where the opinion begins in its official reporter
:param last_page:
the page where the opinion ends in its official reporter
:param decision_date:
date when the opinion was first published by the court
(not the publication date of the reporter volume)
:param court:
name of the court that published the opinion
:param position:
the opinion's attitude toward the court's disposition of the case.
e.g. ``majority``, ``dissenting``, ``concurring``, ``concurring in the result``
:param author:
name of the judge who authored the opinion, if identified
"""
name: str
name_abbreviation: str
citations: Iterable[str]
first_page: Optional[int]
last_page: Optional[int]
decision_date: datetime.date
court: str
position: Optional[str]
author: Optional[str]
text: Optional[str] = field(repr=False)
def __post_init__(self):
self.holdings: List[Holding] = []
self.factors: Dict[
Factor, Union[TextQuoteSelector, Tuple[TextQuoteSelector, ...]]
] = {}
def contradicts(
self, other: Union[Opinion, Holding], context: Optional[ContextRegister] = None
) -> bool:
"""
Test whether ``other`` is or contains a :class:`.Holding` contradicted by ``self``.
:param other:
another :class:`.Opinion` or :class:`.Holding`
:returns:
a bool indicating whether any holding of ``self`` is
inconsistent with the :class:`.Rule` ``other``, or with
any holding of ``other`` if ``other`` is an :class:`.Opinion`.
"""
if isinstance(other, Holding):
return any(
self_holding.contradicts(other, context)
for self_holding in self.holdings
)
elif isinstance(other, self.__class__):
return any(
any(
self_holding.contradicts(other_holding, context)
for self_holding in self.holdings
)
for other_holding in other.holdings
)
raise TypeError(
f"'Contradicts' test not implemented for types {self.__class__} and {other.__class__}."
)
@property
def generic_factors(self) -> List[Factor]:
r"""
Get all generic :class:`.Factor`\s mentioned in ``self``.
:returns:
a list of generic :class:`.Factor` objects mentioned in
any ``input``, ``output``, or ``despite`` :class:`.Factor`
of ``self``, with guaranteed order, including each
generic :class:`.Factor` only once.
"""
return list(
{
generic: None
for holding in self.holdings
for generic in holding.generic_factors
}
)
def get_factor_by_name(self, name: str) -> Optional[Factor]:
"""
Search recursively in holdings of ``self`` for :class:`.Factor` with ``name``.
:param name:
string to match with the ``name`` attribute of the
:class:`.Factor` being searched for.
:returns:
a :class:`.Factor` with the specified ``name``, if one
exists in a :class:`.Rule` in ``self.holdings``.
Otherwise ``None``.
"""
for holding in self.holdings:
factor = holding.get_factor_by_name(name)
if factor is not None:
return factor
return None
def posit_holding(
self,
holding: Union[Holding, Rule],
text_links: Optional[TextLinkDict] = None,
context: Optional[Sequence[Factor]] = None,
) -> None:
r"""Record that this Opinion endorses specified :class:`Holding`\s."""
if isinstance(holding, Rule):
logger.warning(
"posit_holding was called with a Rule "
"that was automatically converted to a Holding"
)
holding = Holding(rule=holding)
if not isinstance(holding, Holding):
raise TypeError('"holding" must be an object of type Holding.')
if context:
holding = holding.new_context(context, context_opinion=self)
self.holdings.append(holding)
if text_links:
for factor in holding.recursive_factors:
for selector in text_links.get(factor, []):
if factor not in self.factors:
self.factors[factor] = [] # repeated elsewhere?
if not any(
selector == known_selector
for known_selector in self.factors[factor]
):
self.factors[factor].append(selector)
def posit_holdings(
self,
holdings: Iterable[Union[Holding, Rule]],
text_links: Optional[TextLinkDict] = None,
context: Optional[Sequence[Factor]] = None,
):
r"""
Add :class:`.Holding`\s to this ``Opinion`` from a sequence.
:param holdings:
a sequence of :class:`.Holding`\s that this :class:`.Opinion`
posits as valid in its own court or jurisdiction, regardless of
whether ``self`` accepts that the ``inputs`` correspond to the
reality of the current case, and regardless of whether the
court orders that the ``outputs`` be put into effect.
:param text_links:
mapping of :class:`Factor`\s to the :class:`Opinion` passages where
they can be found. Can be obtained as the `mentioned` return value
of one of the functions in :mod:`authorityspoke.io.readers`\.
:param context:
an ordered sequence (probably :py:class:`dict`) of
generic :class:`.Factor` objects from ``self`` which
will provide the context for the new holding in the
present case.
"""
for holding in holdings:
self.posit_holding(holding, text_links=text_links, context=context)
def posit(
self,
holdings: Union[Holding, Iterable[Union[Holding, Rule]]],
text_links: Optional[TextLinkDict] = None,
context: Optional[Sequence[Factor]] = None,
) -> None:
r"""
Add one or more :class:`.Holding`\s to this ``Opinion``.
This method passes its values to :meth:`~posit_holding` or
:meth:`~posit_holdings` depending on whether the `holding` parameter
is one :class:`.Holding` or a :class:`list`\.
:param holding:
a :class:`.Holding` that the :class:`.Opinion` ``self`` posits
as valid in its own court or jurisdiction, regardless of
whether ``self`` accepts that the ``inputs`` of the
:class:`.Holding` correspond to the reality of the current
case, and regardless of whether the court orders that
the ``outputs`` of the :class:`.Holding` be put into effect.
:param text_links:
mapping of :class:`Factor`\s to the :class:`Opinion` passages where
they can be found. Can be obtained as the "mentioned" return value
of one of the functions in :mod:`authorityspoke.io.readers`\.
:param context:
an ordered sequence (probably :py:class:`dict`) of
generic :class:`.Factor` objects from ``self`` which
will provide the context for the new holding in the
present case.
"""
if isinstance(holdings, Iterable):
self.posit_holdings(holdings, text_links=text_links, context=context)
else:
self.posit_holding(holdings, text_links=text_links, context=context)
def get_anchors(self, holding: Holding, include_factors: bool = True) -> List[str]:
r"""
Get text passages where a :class:`.Holding` is linked to ``self``.
:param holding:
a holding to find anchors for, which must be in :attr:`~Opinion.holdings`\.
:returns:
a :class:`list` with the text of each passage that anchors the :class:`.Holding`
"""
def add_selectors(
anchor_list: List, selectors: Optional[Iterable[TextQuoteSelector]]
) -> List[str]:
if selectors is None:
return anchor_list
for selector in selectors:
new_text = self.select_text(selector)
if new_text:
anchor_list.append(new_text)
else:
logger.error(
f"Failed to find Opinion text with {selector} in "
+ (f"Opinion {self.name}.")
)
return anchor_list
anchors: List[str] = []
if include_factors:
for factor in holding.rule.procedure.factors_all:
anchors = add_selectors(anchors, self.factors.get(factor))
# What about the Rule? Can that have its own selectors?
anchors = add_selectors(anchors, holding.selectors)
return anchors
def __ge__(self, other: Union[Opinion, Rule]) -> bool:
"""
Find whether ``self``'s holdings imply all the holdings of ``other``.
:returns:
a bool indicating whether the :class:`.Rule` ``other``
(or every holding of ``other``, if other is an :class:`.Opinion`)
is implied by some :class:`.Rule` in ``self.holdings``.
"""
if isinstance(other, (Rule, Holding)):
return any(self_holding >= other for self_holding in self.holdings)
elif isinstance(other, self.__class__):
for other_holding in other.holdings:
if not any(
self_holding >= other_holding for self_holding in self.holdings
):
return False
return True
raise TypeError(
f"'Implies' test not implemented for types {self.__class__} and {other.__class__}."
)
def __gt__(self, other) -> bool:
"""
Find whether ``self``\'s holdings imply ``other``\'s but self != other.
This actually tests for inequality because ``Opinion`` does not
have a ``means`` method.
:returns:
self >= other and self != other.
"""
return (self >= other) and (self != other)
def select_text(self, selector: TextQuoteSelector) -> Optional[str]:
r"""
Get text using a :class:`.TextQuoteSelector`.
:param selector:
a selector referencing a text passage in this :class:`Opinion`.
:returns:
the text referenced by the selector, or ``None`` if the text
can't be found.
"""
if re.search(selector.passage_regex, self.text, re.IGNORECASE):
return selector.exact
raise ValueError(
f'Passage "{selector.exact}" from TextQuoteSelector '
+ f'not found in Opinion "{self}".'
)
def __str__(self):
if isinstance(self.citations, str):
citation = self.citations
else:
citation = self.citations[0]
name = self.name_abbreviation or self.name
if self.position == "majority":
position = ""
else:
position = self.position + " "
return f"<{position}Opinion> {name}, {citation} ({self.decision_date})"