|
38 | 38 | SCHEMA_RELATIVE_IMPORT_TREE = Path(INPUT_DIR) / "imports_relative" / "L0_0" / "L1_0_0" / "main.yaml" |
39 | 39 | SCHEMA_RELATIVE_IMPORT_TREE2 = Path(INPUT_DIR) / "imports_relative" / "L0_2" / "main.yaml" |
40 | 40 |
|
| 41 | +CREATURE_SCHEMA = "creature_schema" |
| 42 | +CREATURE_SCHEMA_BASE_URL = "https://github.com/linkml/linkml-runtime/tests/test_utils/input/mcc" |
| 43 | +CREATURE_SCHEMA_BASE_PATH = Path(INPUT_DIR) / "mcc" |
| 44 | + |
41 | 45 | yaml_loader = YAMLLoader() |
42 | 46 | IS_CURRENT = "is current" |
43 | 47 | EMPLOYED_AT = "employed at" |
@@ -81,6 +85,23 @@ def sv_attributes() -> SchemaView: |
81 | 85 | return SchemaView(os.path.join(INPUT_DIR, "attribute_edge_cases.yaml")) |
82 | 86 |
|
83 | 87 |
|
| 88 | +@pytest.fixture(scope="session") |
| 89 | +def creature_view() -> SchemaView: |
| 90 | + return SchemaView(str(CREATURE_SCHEMA_BASE_PATH / "creature_schema.yaml")) |
| 91 | + |
| 92 | + |
| 93 | +@pytest.fixture(scope="session") |
| 94 | +def creature_view_remote() -> SchemaView: |
| 95 | + """Fixture for a SchemaView for testing remote imports.""" |
| 96 | + return SchemaView(str(CREATURE_SCHEMA_BASE_PATH / "creature_schema_remote.yaml")) |
| 97 | + |
| 98 | + |
| 99 | +@pytest.fixture(scope="session") |
| 100 | +def creature_view_local() -> SchemaView: |
| 101 | + """Fixture for a SchemaView for testing local relative file path imports.""" |
| 102 | + return SchemaView(str(CREATURE_SCHEMA_BASE_PATH / "creature_schema_local.yaml")) |
| 103 | + |
| 104 | + |
84 | 105 | def make_schema( |
85 | 106 | name: str, |
86 | 107 | prefixes: list[Prefix] | None = None, |
@@ -307,6 +328,35 @@ def sv_induced_slots() -> SchemaView: |
307 | 328 | return SchemaView(schema_str) |
308 | 329 |
|
309 | 330 |
|
| 331 | +@pytest.fixture |
| 332 | +def sv_ordering_tests() -> SchemaView: |
| 333 | + """SchemaView for testing class ordering.""" |
| 334 | + schema = """ |
| 335 | +id: https://example.com/ordering-tests |
| 336 | +name: ordering-tests |
| 337 | +classes: |
| 338 | + Clarinet: |
| 339 | + rank: 5 |
| 340 | + is_a: wind instrument |
| 341 | + instrument: |
| 342 | + rank: 2 |
| 343 | + Bassoon: |
| 344 | + is_a: wind instrument |
| 345 | + wind instrument: |
| 346 | + rank: 1 |
| 347 | + is_a: instrument |
| 348 | + Abacus: |
| 349 | + is_a: counting instrument |
| 350 | + counting instrument: |
| 351 | + rank: 4 |
| 352 | + is_a: instrument |
| 353 | + Didgeridoo: |
| 354 | + rank: 3 |
| 355 | + is_a: wind instrument |
| 356 | +""" |
| 357 | + return SchemaView(schema) |
| 358 | + |
| 359 | + |
310 | 360 | def test_imports(schema_view_with_imports: SchemaView) -> None: |
311 | 361 | """View should by default dynamically include imports chain.""" |
312 | 362 | view = schema_view_with_imports |
@@ -697,6 +747,145 @@ def test_uris_without_default_prefix() -> None: |
697 | 747 | assert view.get_uri("test_slot", imports=True) == "https://example.org/test#test_slot" |
698 | 748 |
|
699 | 749 |
|
| 750 | +CREATURE_EXPECTED = { |
| 751 | + "class": { |
| 752 | + CREATURE_SCHEMA: {"MythicalCreature", "HasMagic", "MagicalAbility", "Dragon", "Phoenix", "Unicorn"}, |
| 753 | + "creature_basics": {"Entity", "Creature", "Location", "CreatureAttribute", "HasHabitat"}, |
| 754 | + }, |
| 755 | + "slot": { |
| 756 | + CREATURE_SCHEMA: {"creature_class", "magical_abilities", "level_of_magic"}, |
| 757 | + "creature_basics": {"id", "name", "description", "species", "habitat"}, |
| 758 | + }, |
| 759 | + "type": { |
| 760 | + CREATURE_SCHEMA: {"MagicLevel"}, |
| 761 | + "creature_types": {"string", "integer", "boolean"}, |
| 762 | + }, |
| 763 | + "enum": {CREATURE_SCHEMA: {"CreatureClass"}}, |
| 764 | + "subset": {CREATURE_SCHEMA: set(), "creature_subsets": {"mythical_creature", "generic_creature"}}, |
| 765 | + "element": {}, |
| 766 | +} |
| 767 | + |
| 768 | +# add in the elements |
| 769 | +for value in CREATURE_EXPECTED.values(): |
| 770 | + for s_name, el in value.items(): |
| 771 | + if s_name not in CREATURE_EXPECTED["element"]: |
| 772 | + CREATURE_EXPECTED["element"][s_name] = set() |
| 773 | + CREATURE_EXPECTED["element"][s_name].update(el) |
| 774 | + |
| 775 | + |
| 776 | +@pytest.mark.parametrize("schema", ["creature_view", "creature_view_remote", "creature_view_local"]) |
| 777 | +@pytest.mark.parametrize("entity", CREATURE_EXPECTED.keys()) |
| 778 | +def test_creature_schema_entities_with_without_imports( |
| 779 | + schema: str, entity: str, request: pytest.FixtureRequest |
| 780 | +) -> None: |
| 781 | + """Test retrieval of entities from the creature schema. |
| 782 | +
|
| 783 | + Tests the following methods: |
| 784 | + - all_{entity}s (e.g. all_classes, all_slots, etc.) with and without imports |
| 785 | + - get_{entity} (e.g. get_class, get_slot, etc.) |
| 786 | + - the "from_schema" attribute of the retrieved entities |
| 787 | +
|
| 788 | + The schemas tested are: |
| 789 | + - creature_view: the main schema with all entities |
| 790 | + - creature_view_remote: imports the creature schema using a curie and a remote URL |
| 791 | + - creature_view_local: imports the creature schema using a local relative file path |
| 792 | + """ |
| 793 | + creature_view = request.getfixturevalue(schema) |
| 794 | + |
| 795 | + # use the PLURAL mapping to get the correct method name to retrieve all entities of the given type |
| 796 | + get_all_fn = "all_" + PLURAL.get(entity, f"{entity}s") |
| 797 | + if schema == "creature_view": |
| 798 | + assert set(getattr(creature_view, get_all_fn)(imports=False)) == CREATURE_EXPECTED[entity][CREATURE_SCHEMA] |
| 799 | + else: |
| 800 | + assert set(getattr(creature_view, get_all_fn)(imports=False)) == set() |
| 801 | + |
| 802 | + # merge the values from all the sources to get the complete set of entities |
| 803 | + all_entities = set().union(*CREATURE_EXPECTED[entity].values()) |
| 804 | + assert set(getattr(creature_view, get_all_fn)(imports=True)) == all_entities |
| 805 | + |
| 806 | + # run creature_view.get_{entity} for each entity and check the source |
| 807 | + for src in CREATURE_EXPECTED[entity]: |
| 808 | + for entity_name in CREATURE_EXPECTED[entity][src]: |
| 809 | + e = getattr(creature_view, f"get_{entity}")(entity_name, imports=True) |
| 810 | + assert e.from_schema == f"{CREATURE_SCHEMA_BASE_URL}/{src}" |
| 811 | + |
| 812 | + |
| 813 | +@pytest.mark.parametrize("entity", CREATURE_EXPECTED.keys()) |
| 814 | +def test_get_entities_with_without_imports(creature_view: SchemaView, entity: str) -> None: |
| 815 | + """Test retrieval of a specific entity from the creature schema.""" |
| 816 | + get_fn = f"get_{entity}" |
| 817 | + |
| 818 | + for src in CREATURE_EXPECTED[entity]: |
| 819 | + for entity_name in CREATURE_EXPECTED[entity][src]: |
| 820 | + if src == CREATURE_SCHEMA: |
| 821 | + # if the source is the main schema, we can use the method directly |
| 822 | + e = getattr(creature_view, get_fn)(entity_name, imports=False) |
| 823 | + assert e.name == entity_name |
| 824 | + # N.b. BUG: due to caching and how the `from_schema` element is generated, |
| 825 | + # we cannot know whether it will be populated. |
| 826 | + # assert e.from_schema is None |
| 827 | + else: |
| 828 | + # if the source is an imported schema, we expect None without imports |
| 829 | + assert getattr(creature_view, get_fn)(entity_name, imports=False) is None |
| 830 | + if entity != "element": |
| 831 | + # in strict mode, we expect an error if the entity does not exist |
| 832 | + with pytest.raises(ValueError, match=f'No such {entity}: "{entity_name}"'): |
| 833 | + getattr(creature_view, f"get_{entity}")(entity_name, imports=False, strict=True) |
| 834 | + |
| 835 | + # turn on imports |
| 836 | + e = getattr(creature_view, f"get_{entity}")(entity_name, imports=True) |
| 837 | + assert e.from_schema == f"{CREATURE_SCHEMA_BASE_URL}/{src}" |
| 838 | + |
| 839 | + |
| 840 | +@pytest.mark.parametrize("entity", argvalues=[e for e in CREATURE_EXPECTED if e != "element"]) |
| 841 | +def test_get_entity_does_not_exist(creature_view: SchemaView, entity: str) -> None: |
| 842 | + """Test retrieval of a specific entity from the creature schema.""" |
| 843 | + get_fn = f"get_{entity}" |
| 844 | + |
| 845 | + # returns None unless the `strict` flag is passed |
| 846 | + assert getattr(creature_view, get_fn)("does_not_exist") is None |
| 847 | + |
| 848 | + # raises an error with `strict` flag on |
| 849 | + with pytest.raises(ValueError, match=f'No such {entity}: "does_not_exist"'): |
| 850 | + getattr(creature_view, get_fn)("does_not_exist", strict=True) |
| 851 | + |
| 852 | + |
| 853 | +ORDERING_TESTS = { |
| 854 | + # Bassoon and Abacus are unranked, so appear at the end of the list. |
| 855 | + "rank": ["wind instrument", "instrument", "Didgeridoo", "counting instrument", "Clarinet", "Bassoon", "Abacus"], |
| 856 | + "preserve": [ |
| 857 | + "Clarinet", |
| 858 | + "instrument", |
| 859 | + "Bassoon", |
| 860 | + "wind instrument", |
| 861 | + "Abacus", |
| 862 | + "counting instrument", |
| 863 | + "Didgeridoo", |
| 864 | + ], |
| 865 | + # lexical ordering is case-sensitive, so all the capitalized words come first. |
| 866 | + "lexical": ["Abacus", "Bassoon", "Clarinet", "Didgeridoo", "counting instrument", "instrument", "wind instrument"], |
| 867 | + # TODO: this looks very dodgy |
| 868 | + "inheritance": [ |
| 869 | + "instrument", |
| 870 | + "wind instrument", |
| 871 | + "Clarinet", |
| 872 | + "Bassoon", |
| 873 | + "counting instrument", |
| 874 | + "Abacus", |
| 875 | + "Didgeridoo", |
| 876 | + ], |
| 877 | +} |
| 878 | + |
| 879 | + |
| 880 | +@pytest.mark.parametrize( |
| 881 | + ("ordered_by"), |
| 882 | + ORDERING_TESTS.keys(), |
| 883 | +) |
| 884 | +def test_all_classes_ordered_by(sv_ordering_tests: SchemaView, ordered_by: str) -> None: |
| 885 | + """Test the ordered_by method.""" |
| 886 | + assert list(sv_ordering_tests.all_classes(ordered_by=ordered_by).keys()) == ORDERING_TESTS[ordered_by] |
| 887 | + |
| 888 | + |
700 | 889 | def test_children_method(schema_view_no_imports: SchemaView) -> None: |
701 | 890 | """Test retrieval of the children of a class.""" |
702 | 891 | view = schema_view_no_imports |
|
0 commit comments