Source code for quickly.dom.scope

# -*- coding: utf-8 -*-
# This file is part of `quickly`, a library for LilyPond and the `.ly` format
# Copyright © 2019-2020 by Wilbert Berendsen <>
# This module is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This module is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <>.

The Scope class finds included documents.

import reprlib
from urllib.parse import urljoin, urlparse

import parce.util

import quickly

[docs]class Scope: """A Scope helps finding files included by a parce Document. Initialize Scope with a parce Document. The :attr:`~parce.document.AbstractDocument.url` attribute of the document helps finding included files. That url should be absolute. The ``parent`` is specified when a new Scope is created by :meth:`include_scope`. The ``factory`` keyword parameter and attribute is a callable that should return a :class:`parce.Document` for a filename. If you don't specify a factory, a default one is used that loads a Document from the file system, and if found, caches it using a cache based on the file's mtime. Specify a factory to use other caching, deferred loading, or to look for a document in a list of open documents in a GUI editor etc. If desired, add absolute urls to the :attr:`include_path` instance attribute, indicating folders where to search for includeable files. """ def __init__(self, doc, parent=None, factory=None, node=None): if not factory: factory = parce.util.file_cache(quickly.load).__getitem__ self._document = doc self.parent = parent #: The parent Scope (None for the root Scope) self.factory = factory #: A callable returning a :class:`parce.Document` for a filename. self.node = node #: The node that was specified to :meth:`include_scope`. #: A list of directories to search for \include-d files. self.include_path = parent.include_path if parent else [] #: Whether to search in the directory of an included file for new includes. self.relative_include = parent.relative_include if parent else True def __repr__(self): return "<{} {}>".format(type(self).__name__, reprlib.repr(self.document().url))
[docs] def document(self): """Return our parce Document.""" return self._document
[docs] def include_scope(self, url, node=None): """Return a child scope for the url. If the ``url`` is relative, it is resolved against our document's url (if :attr:`relative_include` is True), the root scope's url and the urls in the :attr:`include_path`. A ``node`` can be given, that's simply put in the :attr:`node` attribute of the returned child scope. It can be used to look further in the document that included the current document, to find e.g. a variable definition. Returns None if no includable document could be found. This scope inherits the factory, the include_path and the relative_include setting of ourselves. """ for u in self.urls(url): doc = self.get_document(u) if doc: return type(self)(doc, self, self.factory, node)
[docs] def ancestors(self): """Yield the ancestor scopes.""" scope = self while scope.parent: scope = scope.parent yield scope
[docs] def root(self): """The root scope.""" scope = self for scope in self.ancestors(): pass return scope
[docs] def urls(self, url): """Return a list of unique urls representing possibly includable files. The list results from the filename of our document (if set and if :attr:`relative_include` is True), the filename of the document that started the include chain, and the include path. The urls are not checked for existence. """ # skip urls already in parent scopes, prevents circular include hangs skip = {self.document().url} skip.update(scope.document().url for scope in self.ancestors()) urls = [] def add(base_url): if base_url: u = urljoin(base_url, url) if u not in skip and u not in urls: urls.append(u) if self.relative_include: add(self.document().url) add(self.root().document().url) for u in self.include_path: if not u.endswith('/'): u += '/' add(u) return urls
[docs] def get_document(self, url): """Return a parce Document at url. Returns None if no document can be found. The default implementation calls :attr:`factory` with a filename pointing to the local file system. OSErrors raised by the factory are suppressed. """ filename = urlparse(url).path try: return self.factory(filename) except OSError: pass