CanLex / tests /test_ingest.py
Beemer
Ingest the AMENDMENTS NOT IN FORCE schedules as bannered pending law
f744415
Raw
History Blame Contribute Delete
4.81 kB
"""Unit tests for Justice Laws XML parsing (canlex/ingest.py).
Offline: builds a tiny Justice Laws-shaped XML document and parses it, so no
network or real corpus is touched. Focuses on the per-section in-force date and
repeal-status metadata.
"""
import tempfile
import unittest
from pathlib import Path
from canlex import ingest
_XML = b"""<?xml version="1.0" encoding="UTF-8"?>
<Statute xmlns:lims="http://justice.gc.ca/lims" lims:current-date="2026-03-31">
<Body>
<Section lims:inforce-start-date="2015-01-01" lims:lastAmendedDate="2023-06-22">
<Label>25</Label>
<MarginalNote>Humanitarian and compassionate considerations</MarginalNote>
<Text>The Minister may grant relief.</Text>
</Section>
<Section lims:inforce-start-date="2018-12-13" lims:lastAmendedDate="2018-12-13">
<Label>13</Label>
<Text><Repealed>[Repealed, 2018, c. 27, s. 174]</Repealed></Text>
</Section>
<Section lims:inforce-start-date="2001-06-28">
<Label>40</Label>
<MarginalNote>Live section with a repealed subsection</MarginalNote>
<Text>Intro.</Text>
<Subsection><Label>(1)</Label><Text>Still in force.</Text></Subsection>
<Subsection><Label>(2)</Label><Text><Repealed>[Repealed, 2020, c. 1]</Repealed></Text></Subsection>
</Section>
</Body>
<Schedule id="RelatedProvs">
<BillPiece><RelatedOrNotInForce>
<Heading level="5" style="nifrp"><TitleText>- 1991, c. 43, s. 10</TitleText></Heading>
<Section><Label>10</Label><Text>A transitional related provision.</Text></Section>
</RelatedOrNotInForce></BillPiece>
</Schedule>
<Schedule id="NifProvs">
<BillPiece><RelatedOrNotInForce>
<Heading level="5" style="nifrp"><TitleText>- 2026, c. 8, s. 3</TitleText></Heading>
<Section><MarginalNote>Coercive control</MarginalNote><Label>3</Label>
<Text>Section 264 is amended by adding a coercive control offence.</Text>
</Section>
</RelatedOrNotInForce></BillPiece>
</Schedule>
</Statute>"""
def _path():
with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as f:
f.write(_XML)
return Path(f.name)
def _parse():
# I-2.5 is a real SOURCES code; parse_legislation only reads its short/
# name/web_base for citation building. Keyed by section, so the NIF chunk
# (section="") is excluded here; NotInForceTests reads it by id.
return {c["section"]: c for c in ingest.parse_legislation(_path(), "I-2.5")
if c["section"]}
class InForceTests(unittest.TestCase):
def test_every_section_carries_its_in_force_date(self):
by_sec = _parse()
self.assertEqual(by_sec["25"]["in_force"], "2015-01-01")
self.assertEqual(by_sec["40"]["in_force"], "2001-06-28")
class AmendmentStubTests(unittest.TestCase):
def test_matches_bare_amendment_markers(self):
for s in ["30 [Amendment]", "162 [Amendments]", "5 [Related provision]",
"5.1 [Related provisions]"]:
self.assertTrue(ingest._AMENDMENT_STUB.match(s), s)
def test_does_not_match_real_or_repealed_content(self):
for s in ["13 [Repealed, 2018, c. 27, s. 174]",
"34 The Minister may [Amendment] later amend...",
"40 Inadmissibility on security grounds"]:
self.assertIsNone(ingest._AMENDMENT_STUB.match(s), s)
class NotInForceTests(unittest.TestCase):
def test_nif_schedule_entry_ingested_as_pending_law(self):
chunks = ingest.parse_legislation(_path(), "I-2.5")
nif = [c for c in chunks if c["id"].startswith("I-2.5-nif-")]
self.assertEqual(len(nif), 1)
c = nif[0]
self.assertEqual(c["status"], "not-in-force")
self.assertEqual(c["section"], "") # never satisfies a lookup
self.assertIn("2026, c. 8, s. 3", c["history"])
self.assertIn("coercive control", c["text"].lower())
self.assertIn("not in force", c["citation"].lower())
def test_related_provisions_schedule_is_not_ingested(self):
chunks = ingest.parse_legislation(_path(), "I-2.5")
self.assertFalse(any("transitional related provision" in c["text"].lower()
for c in chunks))
class RepealStatusTests(unittest.TestCase):
def test_wholly_repealed_section_is_flagged(self):
self.assertEqual(_parse()["13"]["status"], "repealed")
def test_live_section_with_a_repealed_subsection_is_not_flagged(self):
# The <Repealed> sits inside a Subsection, not the section's own Text,
# so the section itself is still in force.
self.assertEqual(_parse()["40"]["status"], "in force")
def test_ordinary_section_is_in_force(self):
self.assertEqual(_parse()["25"]["status"], "in force")
if __name__ == "__main__":
unittest.main()