Skip to content

Commit

Permalink
store absolute path in the AST, simplifies relative import resolution
Browse files Browse the repository at this point in the history
  • Loading branch information
mlin committed Jul 16, 2019
1 parent 38f3784 commit d226e22
Show file tree
Hide file tree
Showing 7 changed files with 174 additions and 154 deletions.
13 changes: 6 additions & 7 deletions WDL/CLI.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,7 @@ def print_error(exn):
else:
if isinstance(getattr(exn, "pos", None), SourcePosition):
print(
"({} Ln {} Col {}) {}".format(
exn.pos.filename, exn.pos.line, exn.pos.column, str(exn)
),
"({} Ln {} Col {}) {}".format(exn.pos.uri, exn.pos.line, exn.pos.column, str(exn)),
file=sys.stderr,
)
else:
Expand Down Expand Up @@ -253,8 +251,9 @@ async def read_source(uri, path, importer_uri):
if uri.startswith("http:") or uri.startswith("https:"):
dn = tempfile.mkdtemp(prefix="miniwdl_import_uri_")
subprocess.check_call(["wget", "-nv", uri], cwd=dn)
with open(glob.glob(dn + "/*")[0], "r") as infile:
return infile.read()
fn = glob.glob(dn + "/*")[0]
with open(fn, "r") as infile:
return ReadSourceResult(infile.read(), os.path.abspath(fn))
return await read_source_default(uri, path, importer_uri)


Expand Down Expand Up @@ -326,7 +325,7 @@ def runner(
except Error.EvalError as exn:
print(
"({} Ln {} Col {}) {}, {}".format(
exn.pos.filename, exn.pos.line, exn.pos.column, exn.__class__.__name__, str(exn)
exn.pos.uri, exn.pos.line, exn.pos.column, exn.__class__.__name__, str(exn)
),
file=sys.stderr,
)
Expand All @@ -338,7 +337,7 @@ def runner(
pos = getattr(exn.__cause__, "pos")
print(
"({} Ln {} Col {}) {}, {}".format(
pos.filename,
pos.uri,
pos.line,
pos.column,
exn.__cause__.__class__.__name__,
Expand Down
27 changes: 20 additions & 7 deletions WDL/Error.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,24 @@
from . import Type


SourcePosition = NamedTuple(
"SourcePosition",
[("filename", str), ("line", int), ("column", int), ("end_line", int), ("end_column", int)],
)
"""Source file, line, and column, attached to each AST node"""
class SourcePosition(
NamedTuple(
"SourcePosition",
[
("uri", str),
("abspath", str),
("line", int),
("column", int),
("end_line", int),
("end_column", int),
],
)
):
"""
Source position attached to AST nodes and exceptions; NamedTuple of ``uri`` the filename/URI
passed to :func:`WDL.load` or a WDL import statement, which may be relative; ``abspath`` the
absolute filename/URI; and int positions ``line`` ``end_line`` ``column`` ``end_column``
"""


class SyntaxError(Exception):
Expand Down Expand Up @@ -53,13 +66,13 @@ def __init__(self, pos: SourcePosition) -> None:
def __lt__(self, rhs: TVSourceNode) -> bool:
if isinstance(rhs, SourceNode):
return (
self.pos.filename,
self.pos.abspath,
self.pos.line,
self.pos.column,
self.pos.end_line,
self.pos.end_column,
) < (
rhs.pos.filename,
rhs.pos.abspath,
rhs.pos.line,
rhs.pos.column,
rhs.pos.end_line,
Expand Down
9 changes: 6 additions & 3 deletions WDL/Lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
assert isinstance(pos, WDL.SourcePosition)
assert isinstance(lint_class, str) and isinstance(message, str)
print(json.dumps({
"filename" : pos.filename,
"uri" : pos.uri,
"abspath" : pos.abspath,
"line" : pos.line,
"end_line" : pos.end_line,
"column" : pos.column,
Expand Down Expand Up @@ -816,7 +817,8 @@ def task(self, obj: Tree.Task) -> Any:
obj,
"SC{} {}".format(item["code"], item["message"]),
Error.SourcePosition(
filename=obj.command.pos.filename,
uri=obj.command.pos.uri,
abspath=obj.command.pos.abspath,
line=line,
column=column,
end_line=line,
Expand Down Expand Up @@ -861,7 +863,8 @@ def task(self, obj: Tree.Task) -> Any:
obj,
"command indented with both spaces & tabs",
Error.SourcePosition(
filename=obj.command.pos.filename,
uri=obj.command.pos.uri,
abspath=obj.command.pos.abspath,
line=obj.command.pos.line + ofs,
column=1,
end_line=obj.command.pos.line + ofs,
Expand Down
43 changes: 27 additions & 16 deletions WDL/Tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -1257,10 +1257,17 @@ def typecheck(self, check_quant: bool = True) -> None:
self.workflow.typecheck(self, check_quant=check_quant)


async def read_source_default(uri: str, path: List[str], importer_uri: Optional[str]) -> str:
if uri.startswith("file://"):
ReadSourceResult = NamedTuple("ReadSourceResult", [("source_text", str), ("abspath", str)])


async def read_source_default(
uri: str, path: List[str], importer: Optional[Document]
) -> ReadSourceResult:
if uri.startswith("file:///"):
uri = uri[7:]
if importer_uri:
if importer:
path = path + [os.path.dirname(importer.pos.abspath)]
"""
if importer_uri.startswith("file://"):
importer_uri = importer_uri[7:]
importer_dir = os.path.dirname(importer_uri)
Expand All @@ -1269,6 +1276,7 @@ async def read_source_default(uri: str, path: List[str], importer_uri: Optional[
morepaths = [os.path.join(p, importer_dir) for p in path]
path.extend(morepaths)
path.append(importer_dir)
"""
# search cwd and path for an extant file
fn = next(
(
Expand All @@ -1282,24 +1290,25 @@ async def read_source_default(uri: str, path: List[str], importer_uri: Optional[
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), uri)
# TODO: actual async I/O here
with open(fn, "r") as infile:
return infile.read()
return ReadSourceResult(source_text=infile.read(), abspath=os.path.abspath(fn))


async def load_async(
uri: str,
path: Optional[List[str]] = None,
check_quant: bool = True,
read_source: Optional[Callable[[str, List[str], Optional[str]], Awaitable[str]]] = None,
read_source: Optional[
Callable[[str, List[str], Optional[Document]], Awaitable[ReadSourceResult]]
] = None,
import_max_depth: int = 10,
importer_uri: Optional[str] = None,
importer: Optional[Document] = None,
) -> Document:
path = list(path) if path is not None else []
read_source = read_source or read_source_default
source_text = await read_source(uri, path, importer_uri)
assert isinstance(source_text, str)
read_rslt = await read_source(uri, path, importer)
# parse the document
doc = _parser.parse_document(source_text, uri=uri)
assert isinstance(doc, Document)
doc = _parser.parse_document(read_rslt.source_text, uri=uri, abspath=read_rslt.abspath)
assert doc.pos.uri == uri and doc.pos.abspath.endswith(os.path.basename(doc.pos.uri))
# recursively descend into document's imports, and store the imported
# documents into doc.imports
# TODO: are we supposed to do something smart for relative imports
Expand All @@ -1315,9 +1324,9 @@ async def load_async(
subdoc = await load_async(
imp.uri,
path=path,
importer_uri=uri,
check_quant=check_quant,
read_source=read_source,
importer=doc,
import_max_depth=(import_max_depth - 1),
)
except Exception as exn:
Expand All @@ -1328,12 +1337,12 @@ async def load_async(
try:
doc.typecheck(check_quant=check_quant)
except Error.ValidationError as exn:
exn.source_text = source_text
exn.source_text = read_rslt.source_text
raise exn
except Error.MultipleValidationErrors as multi:
for exn in multi.exceptions:
if not exn.source_text:
exn.source_text = source_text
exn.source_text = read_rslt.source_text
raise multi
return doc

Expand All @@ -1342,15 +1351,17 @@ def load(
uri: str,
path: Optional[List[str]] = None,
check_quant: bool = True,
read_source: Optional[Callable[[str, List[str], Optional[str]], Awaitable[str]]] = None,
read_source: Optional[
Callable[[str, List[str], Optional[Document]], Awaitable[ReadSourceResult]]
] = None,
import_max_depth: int = 10,
importer_uri: Optional[str] = None,
importer: Optional[Document] = None,
) -> Document:
return asyncio.get_event_loop().run_until_complete(
load_async(
uri,
path=path,
importer_uri=importer_uri,
importer=importer,
check_quant=check_quant,
read_source=read_source,
import_max_depth=import_max_depth,
Expand Down
33 changes: 24 additions & 9 deletions WDL/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ def load(
uri: str,
path: Optional[List[str]] = None,
check_quant: bool = True,
read_source: Optional[Callable[[str, List[str], Optional[str]], Awaitable[str]]] = None,
read_source: Optional[
Callable[[str, List[str], Optional[Document]], Awaitable["ReadSourceResult"]]
] = None,
import_max_depth: int = 10,
) -> Document:
"""
Expand Down Expand Up @@ -58,7 +60,9 @@ async def load_async(
uri: str,
path: Optional[List[str]] = None,
check_quant: bool = True,
read_source: Optional[Callable[[str, List[str], Optional[str]], Awaitable[str]]] = None,
read_source: Optional[
Callable[[str, List[str], Optional[Document]], Awaitable["ReadSourceResult"]]
] = None,
import_max_depth: int = 10,
) -> Document:
"""
Expand All @@ -73,26 +77,37 @@ async def load_async(
)


async def read_source_default(uri: str, path: List[str], importer_uri: Optional[str]) -> str:
class ReadSourceResult(Tree.ReadSourceResult):
"""
The ``NamedTuple`` to be returned by the ``read_source`` routine. Its ``source_text: str`` field
provides the WDL source code, and the ``abspath: str`` field is the absolute filename/URI from
which the source was read (e.g. after resolving a relative path).
"""


async def read_source_default(
uri: str, path: List[str], importer: Optional[Document]
) -> ReadSourceResult:
"""
Default async routine for the ``read_source`` parameter to :func:`load` and :func:`load_async`,
which they use to read the desired WDL document and its imports. This default routine handles
local files only, supplying the search path logic to resolve relative filenames; it fails with
network URIs.
:param uri: Filename/URI to read
:param path: Local directiores to search for relative filename imports. The routine may mutate
this list to control the search path for documents imported from the current one.
:param importer_uri: Filename/URI of the importing document, if any
:returns: WDL source code string
:param uri: Filename/URI to read, which may be relative
:param path: Local directories to search for relative imports
:param importer: The document importing the one here requested, if any; the
``importer.pos.uri`` and ``importer.pos.abspath`` fields may be relevant to
resolve relative imports.
:returns: ``ReadSourceResult(source_text="...", abspath="...")``
Callers may wish to override ``read_source`` with logic to download source code from network
URIs, and for local filenames fall back to ``return await WDL.read_source_default(...)``.
Note: the synchronous :func:`load` merely calls :func:`load_async` on the current
``asyncio.get_event_loop()`` and awaits the result.
"""
return await Tree.read_source_default(uri, path, importer_uri)
return await Tree.read_source_default(uri, path, importer)


def parse_document(txt: str, version: Optional[str] = None, uri: str = "") -> Document:
Expand Down
Loading

0 comments on commit d226e22

Please sign in to comment.