linkml / linkml-runtime

Runtime support for linkml generated models
https://linkml.io/linkml/
Creative Commons Zero v1.0 Universal
22 stars 21 forks source link

Optimize `get_classes_by_slot` round 2 #304

Open sneakers-the-rat opened 4 months ago

sneakers-the-rat commented 4 months ago

Waiting for tests to run on the array branch and browsing through issues and PRs, noticed there was one for optimizing get_classes_by_slot https://github.com/linkml/linkml-runtime/pull/300 and any time there's an induced_slot perf thing i gotta check it out now.

For non-induced slots, #300 is a perf wash (for 100 rounds through all slots in biolink, 6.77 before and 6.81 after), but when i went to profile it with induced slots i was surprised at how long the estimated time for a single round through was supposed to take - roughly 3 hours (~160,000x slower vs non-induced slots).

Using sets is a great idea for removing the step of making something unique at the end. we can take advantage of that further by breaking after the first set addition (since future set additions will do nothing).

the problem is that we are still making basically the most expensive call you can do with schemaview, which is to call induced_slot on every combination of slot and class for a schema. induced_slot is expensive in itself, but it's usually fine to do because you're only calling it in the context of a few classes and the caching can handle how slow it is, but since the call is every slot in the context of every class, no caching can be done.

The fix is relatively simple - since get_classes_by_slot only cares about the existence of the slot on the class, not on its full induced form, there is no need to use induced_slot at all. class_induced_slots iterates over the slots given by class_slots: https://github.com/linkml/linkml-runtime/blob/7c311d975422ddf990ae524fe6d969326c1b9261/linkml_runtime/utils/schemaview.py#L1368

so any slots that class_induced_slots knows about, class_slots knows about. The implementation in this PR also avoids iterating over the classes twice, although that's also a perf wash because iterating through a list and comparing strings takes basically no time.

So for the simple timing function

from linkml_runtime.utils.schemaview import SchemaView
import cProfile

def get_slot_classes():
    sv = SchemaView('biolink.yaml')
    for i in range(100):
        for slot in sv.all_slots(imports=True).values():
            _ = sv.get_classes_by_slot(slot, include_induced=False)

def get_induced_slot_classes():
    sv = SchemaView('biolink.yaml')
    for i in range(1):
        for i, slot in enumerate(sv.all_slots(imports=True).values()):
            _ = sv.get_classes_by_slot(slot, include_induced=True)
            if i >= 4:
                break

cProfile.run('get_slot_classes()', 'before_stats.prof')
cProfile.run('get_induced_slot_classes()', 'before_induced_stats.prof')

where i break out of getting the induced slots after 5 slots because it takes so long,

you get:

version regular per-call induced per-call (s)
before 300 8.234e-05 21.59
after 300 8.092e-05 21.85
this PR 6.672e-05 0.01946

So i'll call the regular difference in the noise and the induced slot call roughly 1100x faster. A full run over biolink takes 12s vs ~3h.

attached profiling results where beforest is before 300, before is 300 and after is this PR.

profiling-get_classes_by_slot.zip

codecov[bot] commented 4 months ago

Codecov Report

All modified and coverable lines are covered by tests :white_check_mark:

Project coverage is 62.88%. Comparing base (27b9158) to head (ee4e4b2). Report is 31 commits behind head on main.

Additional details and impacted files ```diff @@ Coverage Diff @@ ## main #304 +/- ## ======================================= Coverage 62.88% 62.88% ======================================= Files 62 62 Lines 8528 8528 Branches 2436 2435 -1 ======================================= Hits 5363 5363 Misses 2554 2554 Partials 611 611 ```

:umbrella: View full report in Codecov by Sentry.
:loudspeaker: Have feedback on the report? Share it here.

sneakers-the-rat commented 4 months ago

would that be helpful? would that look like just running the main linkml tests with the PR'd version of linkml-runtime, or did you have something else in mind? for now I can just run them and post results

cmungall commented 4 months ago

would that look like just running the main linkml tests with the PR'd version of linkml-runtime

That would work! IMO it's a bit clunky and it's a big ask any time there is a schemaview change but induced slots are central enough it's worth doing here, thx!

sneakers-the-rat commented 4 months ago

So yes test failures, but i honestly can't tell if it's from this or not.. i'll investigate more thoroughly later, but would advise not merging yet. Tests also took 2 hours to run and i can't rly fathom why. will also profile that later.

====================================================== short test summary info =======================================================
FAILED tests/test_issues/test_issue_494.py::test_jsonschema_validation - AssertionError: Regex pattern did not match.
FAILED tests/test_linter/test_cli.py::TestLinterCli::test_config_extends_recommended - AssertionError: 1 != 2
FAILED tests/test_linter/test_cli.py::TestLinterCli::test_directory_of_files - AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmp0wrjounp/schema...
FAILED tests/test_linter/test_cli.py::TestLinterCli::test_no_config - AssertionError: 1 != 2
FAILED tests/test_linter/test_cli.py::TestLinterCli::test_no_schema_errors - AssertionError: 1 != 0
FAILED tests/test_linter/test_cli.py::TestLinterCli::test_processes_dot_files_in_directory_when_a_option_provided - AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmpf87woivi/schema...
FAILED tests/test_linter/test_cli.py::TestLinterCli::test_processes_dot_files_in_directory_when_all_option_provided - AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmps6ym618p/schema...
FAILED tests/test_linter/test_cli.py::TestLinterCli::test_validate_schema - AssertionError: 1 != 2
FAILED tests/test_linter/test_rule_canonical_prefixes.py::TestCanonicalPrefixesRule::test_custom_context - AttributeError: 'NoneType' object has no attribute 'append'
FAILED tests/test_linter/test_rule_canonical_prefixes.py::TestCanonicalPrefixesRule::test_default_merged_context - AttributeError: 'NoneType' object has no attribute 'append'
ERROR tests/test_workspaces/test_example_runner.py::test_load_from_dict - AttributeError: 'NoneType' object has no attribute 'append'
ERROR tests/test_workspaces/test_example_runner.py::test_example_runner - AttributeError: 'NoneType' object has no attribute 'append'
ERROR tests/test_workspaces/test_example_runner.py::test_example_runner_non_defaults - AttributeError: 'NoneType' object has no attribute 'append'
========== 10 failed, 6151 passed, 964 skipped, 3 xfailed, 4 warnings, 3 errors, 261 subtests passed in 6882.77s (1:54:42) ===========
expand/collapse pytest output ``` =============================================================== ERRORS =============================================================== _______________________________________________ ERROR at setup of test_load_from_dict ________________________________________________ input_path = .get_path at 0x144044e00> tmp_path = PosixPath('/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/pytest-of-jonny/pytest-69/test_load_from_dict0') @pytest.fixture def example_runner(input_path, tmp_path): schemaview = SchemaView(input_path("personinfo.yaml")) > ctxt = load_multi_context(["obo", "linked_data", "prefixcc"]) tests/test_workspaces/test_example_runner.py:19: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/io/parser.py:46: in load_multi_context ctxt.combine(load_context(n, refresh=refresh)) ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:161: in combine self.add_prefix(pe.prefix, pe.namespace, pe.status) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = Context(name='obo+linked_data+prefixcc', description=None, prefix_expansions=[], comments=None, location=None, format=..., merged_from=None, upper=None, lower=None, _prefixes=None, _prefixes_lower=[], _namespaces=None, _namespaces_lower=[]) prefix = 'AAO', namespace = 'http://purl.obolibrary.org/obo/AAO_', status = , preferred = False def add_prefix( self, prefix: PREFIX, namespace: NAMESPACE, status: StatusType = StatusType.canonical, preferred: bool = False, ): """ Adds a prefix expansion to this context. The current context stays canonical. Additional prefixes added may be classified as non-canonical. If upper or lower is set for this context, the prefix will be auto-case normalized, UNLESS preferred=True :param prefix: prefix to be added :param namespace: namespace to be added :param status: the status of the prefix being added :param preferred: :return: """ # TODO: check status if not preferred: if self.upper: prefix = prefix.upper() if self.lower: raise ValueError("Cannot set both upper AND lower") if self.lower: prefix = prefix.lower() prefixes = self.prefixes(lower=True) namespaces = self.namespaces(lower=True) if prefix.lower() in prefixes: if namespace.lower() in namespaces: return # status = StatusType.multi_alias else: status = StatusType.prefix_alias self._namespaces.append(namespace) self._namespaces_lower.append(namespace.lower()) else: > self._prefixes.append(prefix) E AttributeError: 'NoneType' object has no attribute 'append' ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:205: AttributeError _______________________________________________ ERROR at setup of test_example_runner ________________________________________________ input_path = .get_path at 0x144044a40> tmp_path = PosixPath('/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/pytest-of-jonny/pytest-69/test_example_runner0') @pytest.fixture def example_runner(input_path, tmp_path): schemaview = SchemaView(input_path("personinfo.yaml")) > ctxt = load_multi_context(["obo", "linked_data", "prefixcc"]) tests/test_workspaces/test_example_runner.py:19: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/io/parser.py:46: in load_multi_context ctxt.combine(load_context(n, refresh=refresh)) ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:161: in combine self.add_prefix(pe.prefix, pe.namespace, pe.status) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = Context(name='obo+linked_data+prefixcc', description=None, prefix_expansions=[], comments=None, location=None, format=..., merged_from=None, upper=None, lower=None, _prefixes=None, _prefixes_lower=[], _namespaces=None, _namespaces_lower=[]) prefix = 'AAO', namespace = 'http://purl.obolibrary.org/obo/AAO_', status = , preferred = False def add_prefix( self, prefix: PREFIX, namespace: NAMESPACE, status: StatusType = StatusType.canonical, preferred: bool = False, ): """ Adds a prefix expansion to this context. The current context stays canonical. Additional prefixes added may be classified as non-canonical. If upper or lower is set for this context, the prefix will be auto-case normalized, UNLESS preferred=True :param prefix: prefix to be added :param namespace: namespace to be added :param status: the status of the prefix being added :param preferred: :return: """ # TODO: check status if not preferred: if self.upper: prefix = prefix.upper() if self.lower: raise ValueError("Cannot set both upper AND lower") if self.lower: prefix = prefix.lower() prefixes = self.prefixes(lower=True) namespaces = self.namespaces(lower=True) if prefix.lower() in prefixes: if namespace.lower() in namespaces: return # status = StatusType.multi_alias else: status = StatusType.prefix_alias self._namespaces.append(namespace) self._namespaces_lower.append(namespace.lower()) else: > self._prefixes.append(prefix) E AttributeError: 'NoneType' object has no attribute 'append' ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:205: AttributeError _________________________________________ ERROR at setup of test_example_runner_non_defaults _________________________________________ input_path = .get_path at 0x1440474c0> tmp_path = PosixPath('/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/pytest-of-jonny/pytest-69/test_example_runner_non_defaul0') @pytest.fixture def example_runner(input_path, tmp_path): schemaview = SchemaView(input_path("personinfo.yaml")) > ctxt = load_multi_context(["obo", "linked_data", "prefixcc"]) tests/test_workspaces/test_example_runner.py:19: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/io/parser.py:46: in load_multi_context ctxt.combine(load_context(n, refresh=refresh)) ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:161: in combine self.add_prefix(pe.prefix, pe.namespace, pe.status) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = Context(name='obo+linked_data+prefixcc', description=None, prefix_expansions=[], comments=None, location=None, format=..., merged_from=None, upper=None, lower=None, _prefixes=None, _prefixes_lower=[], _namespaces=None, _namespaces_lower=[]) prefix = 'AAO', namespace = 'http://purl.obolibrary.org/obo/AAO_', status = , preferred = False def add_prefix( self, prefix: PREFIX, namespace: NAMESPACE, status: StatusType = StatusType.canonical, preferred: bool = False, ): """ Adds a prefix expansion to this context. The current context stays canonical. Additional prefixes added may be classified as non-canonical. If upper or lower is set for this context, the prefix will be auto-case normalized, UNLESS preferred=True :param prefix: prefix to be added :param namespace: namespace to be added :param status: the status of the prefix being added :param preferred: :return: """ # TODO: check status if not preferred: if self.upper: prefix = prefix.upper() if self.lower: raise ValueError("Cannot set both upper AND lower") if self.lower: prefix = prefix.lower() prefixes = self.prefixes(lower=True) namespaces = self.namespaces(lower=True) if prefix.lower() in prefixes: if namespace.lower() in namespaces: return # status = StatusType.multi_alias else: status = StatusType.prefix_alias self._namespaces.append(namespace) self._namespaces_lower.append(namespace.lower()) else: > self._prefixes.append(prefix) E AttributeError: 'NoneType' object has no attribute 'append' ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:205: AttributeError ============================================================== FAILURES ============================================================== _____________________________________________________ test_jsonschema_validation _____________________________________________________ input_path = .get_path at 0x13de5f880> def test_jsonschema_validation(input_path): """Test case that tries to infer the target class for a schema that has a local import and doesn't explicitly specify the target class""" SCHEMA = input_path("issue_494/slot_usage_example.yaml") sv = SchemaView(schema=SCHEMA) with pytest.raises( RuntimeError, match="Multiple potential target " r"classes found: \['Container', 'annotation'\]. " "Please specify a target using --target-class " "or by adding tree_root: true to the relevant class in the schema", ): # infer target_class rather than explicit specification > infer_root_class(sv) tests/test_issues/test_issue_494.py:22: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ sv = SchemaView(schema=SchemaDefinition(name='personinfo', id_prefixes=[], id_prefixes_are_closed=None, definition_uri=None...one, slot_names_unique=None, settings={})}, importmap={}, modifications=0, uuid='70a0f3ff-b715-4b8b-83f3-9856da459a2f') def infer_root_class(sv: SchemaView) -> Optional[ClassDefinitionName]: """ Infer the class that should be at the root of the object tree (Note this is distinct from the root of the class hierarchy) If a class is explicitly designated with tree_root, use this. Otherwise use the class that is not referenced as a range in any other class. """ for c in sv.all_classes().values(): if c.tree_root: return c.name refs = defaultdict(int) for cn in sv.all_classes().keys(): for sn in sv.class_slots(cn): slot = sv.induced_slot(sn, cn) r = slot.range if r in sv.all_classes(): for a in sv.class_ancestors(r): refs[a] += 1 candidates = [cn for cn in sv.all_classes().keys() if cn not in refs] # throw Exception if unambiguous root cannot be inferred if len(candidates) > 1: > raise RuntimeError( f"Multiple potential target classes found: {candidates}. " "Please specify a target using --target-class or by adding " "tree_root: true to the relevant class in the schema." ) E RuntimeError: Multiple potential target classes found: ['annotation', 'Container']. Please specify a target using --target-class or by adding tree_root: true to the relevant class in the schema. linkml/utils/datautils.py:93: RuntimeError During handling of the above exception, another exception occurred: input_path = .get_path at 0x13de5f880> def test_jsonschema_validation(input_path): """Test case that tries to infer the target class for a schema that has a local import and doesn't explicitly specify the target class""" SCHEMA = input_path("issue_494/slot_usage_example.yaml") sv = SchemaView(schema=SCHEMA) > with pytest.raises( RuntimeError, match="Multiple potential target " r"classes found: \['Container', 'annotation'\]. " "Please specify a target using --target-class " "or by adding tree_root: true to the relevant class in the schema", ): E AssertionError: Regex pattern did not match. E Regex: "Multiple potential target classes found: \\['Container', 'annotation'\\]. Please specify a target using --target-class or by adding tree_root: true to the relevant class in the schema" E Input: "Multiple potential target classes found: ['annotation', 'Container']. Please specify a target using --target-class or by adding tree_root: true to the relevant class in the schema." tests/test_issues/test_issue_494.py:14: AssertionError --------------------------------------------------------- Captured log call ---------------------------------------------------------- INFO root:schemaview.py:206 Importing testme as testme from source /Users/jonny/git/forks/linkml/tests/test_issues/input/issue_494/slot_usage_example.yaml; base_dir=/Users/jonny/git/forks/linkml/tests/test_issues/input/issue_494 INFO root:schemaview.py:206 Importing linkml:types as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/types from source /Users/jonny/git/forks/linkml/tests/test_issues/input/issue_494/slot_usage_example.yaml; base_dir=None ___________________________________________ TestLinterCli.test_config_extends_recommended ____________________________________________ self = def test_config_extends_recommended(self): config_file = "config.yaml" with self.runner.isolated_filesystem(): write_schema_file() write_config_file(config_file, extends_recommended=True) result = self.runner.invoke(main, ["--config", config_file, SCHEMA_FILE]) > self.assertEqual(result.exit_code, 2) E AssertionError: 1 != 2 tests/test_linter/test_cli.py:110: AssertionError --------------------------------------------------------- Captured log call ---------------------------------------------------------- INFO root:schemaview.py:206 Importing linkml:units as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/units from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None INFO root:schemaview.py:206 Importing linkml:mappings as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/mappings from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None INFO root:schemaview.py:206 Importing linkml:types as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/types from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None INFO root:schemaview.py:206 Importing linkml:annotations as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/annotations from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None INFO root:schemaview.py:206 Importing linkml:extensions as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/extensions from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None _______________________________________________ TestLinterCli.test_directory_of_files ________________________________________________ self = def test_directory_of_files(self): schema_dir = Path("schemas") schema_a = schema_dir / "schema_a.yaml" schema_b = schema_dir / "schema_b.yaml" with self.runner.isolated_filesystem(): schema_dir.mkdir() schema_a.write_text( """ id: http://example.org/test_a name: test_a classes: person: description: An individual human """ ) schema_b.write_text( """ id: http://example.org/test_b name: test_b slots: a slot: description: A slot to hold thing """ ) result = self.runner.invoke(main, [str(schema_dir)]) self.assertEqual(result.exit_code, 1) > self.assertIn(str(schema_a), result.stdout) E AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmp0wrjounp/schemas/schema_b.yaml\n warning Slot has name 'a slot' (standard_naming)\n" tests/test_linter/test_cli.py:215: AssertionError ____________________________________________________ TestLinterCli.test_no_config ____________________________________________________ self = def test_no_config(self): with self.runner.isolated_filesystem(): write_schema_file() result = self.runner.invoke(main, [SCHEMA_FILE]) > self.assertEqual(result.exit_code, 2) E AssertionError: 1 != 2 tests/test_linter/test_cli.py:65: AssertionError ________________________________________________ TestLinterCli.test_no_schema_errors _________________________________________________ self = def test_no_schema_errors(self): with self.runner.isolated_filesystem(): with open(SCHEMA_FILE, "w") as f: f.write( """ id: http://example.org/test name: test classes: Person: description: An individual human """ ) result = self.runner.invoke(main, [SCHEMA_FILE]) > self.assertEqual(result.exit_code, 0) E AssertionError: 1 != 0 tests/test_linter/test_cli.py:184: AssertionError _____________________________ TestLinterCli.test_processes_dot_files_in_directory_when_a_option_provided _____________________________ self = def test_processes_dot_files_in_directory_when_a_option_provided(self): schema_dir = Path("schemas") schema_a = schema_dir / "schema_a.yaml" schema_b = schema_dir / ".schema_b.yaml" with self.runner.isolated_filesystem(): schema_dir.mkdir() schema_a.write_text( """ id: http://example.org/test_a name: test_a classes: person: description: An individual human """ ) schema_b.write_text( """ id: http://example.org/test_b name: test_b slots: a slot: description: A slot to hold thing """ ) result = self.runner.invoke(main, ["-a", str(schema_dir)]) self.assertEqual(result.exit_code, 1) > self.assertIn(str(schema_a), result.stdout) E AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmpf87woivi/schemas/.schema_b.yaml\n warning Slot has name 'a slot' (standard_naming)\n" tests/test_linter/test_cli.py:283: AssertionError ____________________________ TestLinterCli.test_processes_dot_files_in_directory_when_all_option_provided ____________________________ self = def test_processes_dot_files_in_directory_when_all_option_provided(self): schema_dir = Path("schemas") schema_a = schema_dir / "schema_a.yaml" schema_b = schema_dir / ".schema_b.yaml" with self.runner.isolated_filesystem(): schema_dir.mkdir() schema_a.write_text( """ id: http://example.org/test_a name: test_a classes: person: description: An individual human """ ) schema_b.write_text( """ id: http://example.org/test_b name: test_b slots: a slot: description: A slot to hold thing """ ) result = self.runner.invoke(main, ["--all", str(schema_dir)]) self.assertEqual(result.exit_code, 1) > self.assertIn(str(schema_a), result.stdout) E AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmps6ym618p/schemas/.schema_b.yaml\n warning Slot has name 'a slot' (standard_naming)\n" tests/test_linter/test_cli.py:317: AssertionError _________________________________________________ TestLinterCli.test_validate_schema _________________________________________________ self = def test_validate_schema(self): with self.runner.isolated_filesystem(): with open(SCHEMA_FILE, "w") as f: f.write( """ id: http://example.org/test classes: person: description: a person """ ) result = self.runner.invoke(main, ["--validate", SCHEMA_FILE]) > self.assertEqual(result.exit_code, 2) E AssertionError: 1 != 2 tests/test_linter/test_cli.py:335: AssertionError --------------------------------------------------------- Captured log call ---------------------------------------------------------- INFO root:generator.py:199 Using SchemaView with im=None INFO root:schemaview.py:206 Importing linkml:units as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/units from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None INFO root:schemaview.py:206 Importing linkml:mappings as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/mappings from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None INFO root:schemaview.py:206 Importing linkml:types as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/types from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None INFO root:schemaview.py:206 Importing linkml:annotations as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/annotations from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None INFO root:schemaview.py:206 Importing linkml:extensions as /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/extensions from source /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/linkml_runtime/linkml_model/model/schema/meta.yaml; base_dir=None ___________________________________________ TestCanonicalPrefixesRule.test_custom_context ____________________________________________ self = def test_custom_context(self): builder = SchemaBuilder() builder.add_prefix("CHEBI", "http://purl.obolibrary.org/obo/CHEBI_") builder.add_prefix("GEO", "http://identifiers.org/geo/") builder.add_prefix("UPHENO", "http://example.com/wrong/upheno_") builder.add_prefix("WRONG", "http://identifiers.org/orcid/") schema_view = SchemaView(builder.schema) config = CanonicalPrefixesConfig( level=RuleLevel.error, prefixmaps_contexts=["bioregistry.upper", "prefixcc", "obo"], ) rule = CanonicalPrefixesRule(config) > problems = list(rule.check(schema_view)) tests/test_linter/test_rule_canonical_prefixes.py:52: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ linkml/linter/rules.py:256: in check context = load_multi_context(self.config.prefixmaps_contexts) ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/io/parser.py:46: in load_multi_context ctxt.combine(load_context(n, refresh=refresh)) ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:161: in combine self.add_prefix(pe.prefix, pe.namespace, pe.status) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = Context(name='bioregistry.upper+prefixcc+obo', description=None, prefix_expansions=[], comments=None, location=None, f..., merged_from=None, upper=None, lower=None, _prefixes=None, _prefixes_lower=[], _namespaces=None, _namespaces_lower=[]) prefix = '3DMET', namespace = 'http://identifiers.org/3dmet/', status = , preferred = False def add_prefix( self, prefix: PREFIX, namespace: NAMESPACE, status: StatusType = StatusType.canonical, preferred: bool = False, ): """ Adds a prefix expansion to this context. The current context stays canonical. Additional prefixes added may be classified as non-canonical. If upper or lower is set for this context, the prefix will be auto-case normalized, UNLESS preferred=True :param prefix: prefix to be added :param namespace: namespace to be added :param status: the status of the prefix being added :param preferred: :return: """ # TODO: check status if not preferred: if self.upper: prefix = prefix.upper() if self.lower: raise ValueError("Cannot set both upper AND lower") if self.lower: prefix = prefix.lower() prefixes = self.prefixes(lower=True) namespaces = self.namespaces(lower=True) if prefix.lower() in prefixes: if namespace.lower() in namespaces: return # status = StatusType.multi_alias else: status = StatusType.prefix_alias self._namespaces.append(namespace) self._namespaces_lower.append(namespace.lower()) else: > self._prefixes.append(prefix) E AttributeError: 'NoneType' object has no attribute 'append' ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:205: AttributeError _______________________________________ TestCanonicalPrefixesRule.test_default_merged_context ________________________________________ self = def test_default_merged_context(self): builder = SchemaBuilder() builder.add_prefix("CHEBI", "http://purl.obolibrary.org/obo/CHEBI_") builder.add_prefix("GEO", "http://purl.obolibrary.org/obo/GEO_") builder.add_prefix("UPHENO", "http://example.com/wrong/upheno_") builder.add_prefix("WRONG", "http://identifiers.org/orcid/") schema_view = SchemaView(builder.schema) config = CanonicalPrefixesConfig(level=RuleLevel.error, prefixmaps_contexts=["merged"]) rule = CanonicalPrefixesRule(config) > problems = list(rule.check(schema_view)) tests/test_linter/test_rule_canonical_prefixes.py:22: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ linkml/linter/rules.py:256: in check context = load_multi_context(self.config.prefixmaps_contexts) ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/io/parser.py:46: in load_multi_context ctxt.combine(load_context(n, refresh=refresh)) ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:161: in combine self.add_prefix(pe.prefix, pe.namespace, pe.status) _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = Context(name='merged', description=None, prefix_expansions=[], comments=None, location=None, format=None, merged_from=None, upper=None, lower=None, _prefixes=None, _prefixes_lower=[], _namespaces=None, _namespaces_lower=[]) prefix = '3DMET', namespace = 'http://identifiers.org/3dmet/', status = , preferred = False def add_prefix( self, prefix: PREFIX, namespace: NAMESPACE, status: StatusType = StatusType.canonical, preferred: bool = False, ): """ Adds a prefix expansion to this context. The current context stays canonical. Additional prefixes added may be classified as non-canonical. If upper or lower is set for this context, the prefix will be auto-case normalized, UNLESS preferred=True :param prefix: prefix to be added :param namespace: namespace to be added :param status: the status of the prefix being added :param preferred: :return: """ # TODO: check status if not preferred: if self.upper: prefix = prefix.upper() if self.lower: raise ValueError("Cannot set both upper AND lower") if self.lower: prefix = prefix.lower() prefixes = self.prefixes(lower=True) namespaces = self.namespaces(lower=True) if prefix.lower() in prefixes: if namespace.lower() in namespaces: return # status = StatusType.multi_alias else: status = StatusType.prefix_alias self._namespaces.append(namespace) self._namespaces_lower.append(namespace.lower()) else: > self._prefixes.append(prefix) E AttributeError: 'NoneType' object has no attribute 'append' ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/prefixmaps/datamodel/context.py:205: AttributeError ========================================================== warnings summary ========================================================== linkml/utils/generator.py:685 /Users/jonny/git/forks/linkml/linkml/utils/generator.py:685: DeprecationWarning: invalid escape sequence '\_' """ ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/pyshexc/parser/ShExDocLexer.py:4 /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/pyshexc/parser/ShExDocLexer.py:4: DeprecationWarning: typing.io is deprecated, import directly from typing instead. typing.io will be removed in Python 3.12. from typing.io import TextIO linkml/linter/linter.py:38 /Users/jonny/git/forks/linkml/linkml/linter/linter.py:38: DeprecationWarning: Importing Validator directly from the jsonschema package is deprecated and will become an ImportError. Import it from jsonschema.protocols instead. def get_metamodel_validator() -> jsonschema.Validator: ../../../Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/jupyter_client/connect.py:22 /Users/jonny/Library/Caches/pypoetry/virtualenvs/linkml-G-1YJs6r-py3.11/lib/python3.11/site-packages/jupyter_client/connect.py:22: DeprecationWarning: Jupyter is migrating its paths to use standard platformdirs given by the platformdirs library. To remove this warning and see the appropriate new directories, set the environment variable `JUPYTER_PLATFORM_DIRS=1` and then run `jupyter --paths`. The use of platformdirs will be the default in `jupyter_core` v6 from jupyter_core.paths import jupyter_data_dir, jupyter_runtime_dir, secure_write -- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html ====================================================== short test summary info ======================================================= FAILED tests/test_issues/test_issue_494.py::test_jsonschema_validation - AssertionError: Regex pattern did not match. FAILED tests/test_linter/test_cli.py::TestLinterCli::test_config_extends_recommended - AssertionError: 1 != 2 FAILED tests/test_linter/test_cli.py::TestLinterCli::test_directory_of_files - AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmp0wrjounp/schema... FAILED tests/test_linter/test_cli.py::TestLinterCli::test_no_config - AssertionError: 1 != 2 FAILED tests/test_linter/test_cli.py::TestLinterCli::test_no_schema_errors - AssertionError: 1 != 0 FAILED tests/test_linter/test_cli.py::TestLinterCli::test_processes_dot_files_in_directory_when_a_option_provided - AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmpf87woivi/schema... FAILED tests/test_linter/test_cli.py::TestLinterCli::test_processes_dot_files_in_directory_when_all_option_provided - AssertionError: 'schemas/schema_a.yaml' not found in "/private/var/folders/b8/28n_6_zn7flgh40028jxd8j80000gn/T/tmps6ym618p/schema... FAILED tests/test_linter/test_cli.py::TestLinterCli::test_validate_schema - AssertionError: 1 != 2 FAILED tests/test_linter/test_rule_canonical_prefixes.py::TestCanonicalPrefixesRule::test_custom_context - AttributeError: 'NoneType' object has no attribute 'append' FAILED tests/test_linter/test_rule_canonical_prefixes.py::TestCanonicalPrefixesRule::test_default_merged_context - AttributeError: 'NoneType' object has no attribute 'append' ERROR tests/test_workspaces/test_example_runner.py::test_load_from_dict - AttributeError: 'NoneType' object has no attribute 'append' ERROR tests/test_workspaces/test_example_runner.py::test_example_runner - AttributeError: 'NoneType' object has no attribute 'append' ERROR tests/test_workspaces/test_example_runner.py::test_example_runner_non_defaults - AttributeError: 'NoneType' object has no attribute 'append' ```
sneakers-the-rat commented 4 months ago

OK figured out what was wrong - it was just that i had installed my local version of prefixmaps in my linkml environment to test https://github.com/linkml/prefixmaps/pull/69 and hadn't fixed a bug in it.

tests pass now, except for test_issue_494.py which fails because of this, which is pending in another PR: https://github.com/linkml/linkml/pull/1704/commits/f148a1882df1fd162f24b6dc8b192491116dfa16

this is good 2 merge i think, but go ahead and test it urself!

cmungall commented 3 months ago

triggering a manual build to test the new workflow in

sneakers-the-rat commented 3 months ago

Omg I hope it works, lmk if it doesnt and can debug

sneakers-the-rat commented 3 months ago

Ok now that the action is in main i can test it on my fork. Sorry for the runaround with these CI actions, its specifically a weird thing for forks. Ill make it work