Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix multilayer relative imports #368

Open
wants to merge 5 commits into
base: main
Choose a base branch
from

Conversation

will-0
Copy link

@will-0 will-0 commented Feb 9, 2025

Originally mentioned in this issue on the linkml repo.

When you attempt to use relative imports from a file that, itself, has been imported with a relative import, path resolution fails. This bug occurs both in linkml and linkml-runtime. In linkml-runtime, the problem occurs in schemaview.py:

if sn.startswith('.') and ':' not in i:
       i = os.path.normpath(str(Path(sn).parent / i))

If both sn and i are relative file paths, the path added to todo is an absolute path (at least on Mac). When i later than becomes sn, it's not picked up by the above clause and anything it imports does not hit the above condition and therefore just defined relative to the base file.

Marking as draft. @sneakers-the-rat noticed you were the last to edit this section, specifically here. Would appreciate input as to whether there's a reason for the above / it is expected behaviour and I'm missing something.

@sneakers-the-rat
Copy link
Contributor

can you add a set of minimal schemas that demonstrates the bug to the tests?

see:

def test_imports_relative():

https://github.com/linkml/linkml-runtime/tree/main/tests/test_utils/input/imports_relative

@sneakers-the-rat
Copy link
Contributor

related to: #350

@will-0
Copy link
Author

will-0 commented Feb 16, 2025

@sneakers-the-rat test added that fails with original implementation, and fix that passes all tests.

Noted that some other work has been done to allow relative imports without './' prefix in #350. Can try and reconcile with the above if necessary.

@will-0 will-0 marked this pull request as ready for review February 16, 2025 12:52
Copy link
Contributor

@sneakers-the-rat sneakers-the-rat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

seems fine, does need to be synchronized with the other changes that are being made to this exact place tho :)

Comment on lines -293 to -305
# resolve relative imports relative to the importing schema, rather than the
# origin schema. Imports can be a URI or Curie, and imports from the same
# directory don't require a ./, so if the current (sn) import is a relative
# path, and the target import doesn't have : (as in a curie or a URI)
# we prepend the relative path. This WILL make the key in the `schema_map` not
# equal to the literal text specified in the importing schema, but this is
# essential to sensible deduplication: eg. for
# - main.yaml (imports ./types.yaml, ./subdir/subschema.yaml)
# - types.yaml
# - subdir/subschema.yaml (imports ./types.yaml)
# - subdir/types.yaml
# we should treat the two `types.yaml` as separate schemas from the POV of the
# origin schema.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why strip out the comment?

else:
temp = i
i = resolve_import(sn, i)
_ = sn, temp, i
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is this doing?

Comment on lines +306 to +308
else:
temp = i
i = resolve_import(sn, i)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

mild suggest:

Suggested change
else:
temp = i
i = resolve_import(sn, i)
temp = i
i = resolve_import(sn, i)

everything after

if (condition):
    continue

is an else, but avoids needing to go 6 levels of indentation if we can avoid it

@@ -104,6 +105,19 @@ def is_absolute_path(path: str) -> bool:
drive, tail = os.path.splitdrive(norm_path)
return bool(drive and tail)

def resolve_import(sn: str, i: str) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if we're splitting this out to a separate function, it would be great to not have the arguments be named sn and i

@@ -104,6 +105,19 @@ def is_absolute_path(path: str) -> bool:
drive, tail = os.path.splitdrive(norm_path)
return bool(drive and tail)

def resolve_import(sn: str, i: str) -> str:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def resolve_import(sn: str, i: str) -> str:
def _resolve_import(sn: str, i: str) -> str:

unless we expect this to be used elsewhere, probably make it private. it seems pretty specific to this one case in schemaview

Comment on lines +114 to +120
path = os.path.normpath(str(Path(sn).parent / i))
if i.startswith(".") and not path.startswith("."):
# Above condition handles cases where both sn and i are relative paths, and path is not already relative
return f"./{path}"
else:
return path

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we are already treating everything that isn't absolute or has a scheme as a path, i think this would be double-fixing the problem. e.g. tests pass with this

Suggested change
path = os.path.normpath(str(Path(sn).parent / i))
if i.startswith(".") and not path.startswith("."):
# Above condition handles cases where both sn and i are relative paths, and path is not already relative
return f"./{path}"
else:
return path
return os.path.normpath(str(Path(sn).parent / i))

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants