From cb28b62206f1f8430823c7fe3da14da123718ca8 Mon Sep 17 00:00:00 2001 From: Thomas Smith Date: Sun, 17 Feb 2019 17:02:13 -0500 Subject: [PATCH 1/3] Improved mapping of resource paths and file paths. --- st3/sublime_lib/resource_path.py | 156 +++++++++++++++++++++++-------- 1 file changed, 117 insertions(+), 39 deletions(-) diff --git a/st3/sublime_lib/resource_path.py b/st3/sublime_lib/resource_path.py index 5a63fa9..245fb0c 100644 --- a/st3/sublime_lib/resource_path.py +++ b/st3/sublime_lib/resource_path.py @@ -11,18 +11,102 @@ __all__ = ['ResourcePath'] -def get_resource_roots(): - return { - 'Packages': sublime.packages_path(), - 'Cache': sublime.cache_path(), - } +def _abs_parts(path): + if path.root: + return (path.drive, path.root) + path.parts[1:] + else: + return path.parts + +def _file_relative_to(path, base): + """ + Like Path.relative_to, except: + + - `base` must be a single Path object. + - The error message is blank. + - Only a tuple of parts is returned. + + Surprisingly, this is much, much faster. + """ + child_parts = _abs_parts(path) + base_parts = _abs_parts(base) + + n = len(base_parts) + cf = path._flavour.casefold_parts + + if n == 0: + compare = (path.root or path.drive) + else: + compare = cf(child_parts[:n]) + + if compare != cf(base_parts): + raise ValueError() + + return child_parts[n:] + + +class ResourceRoot(): + def __init__(self, root, path): + self.resource_root = ResourcePath(root) + self.file_root = Path(path) + + def resource_to_file_path(self, resource_path): + resource_path = ResourcePath(resource_path) + + parts = resource_path.relative_to(self.resource_root) + if parts == (): + return self.file_root + else: + return self._package_file_path(*parts) + + def file_to_resource_path(self, file_path): + file_path = wrap_path(file_path) + + if not file_path.is_absolute(): + raise ValueError("Cannot convert a relative file path to a resource path.") + + parts = _file_relative_to(file_path, self.file_root) + # parts = file_path.relative_to(self.file_root).parts + if parts == (): + return self.resource_root + else: + return self._package_resource_path(*parts) + + +class DirectoryResourceRoot(ResourceRoot): + def _package_file_path(self, *parts): + return self.file_root.joinpath(*parts) + + def _package_resource_path(self, *parts): + return self.resource_root.joinpath(*parts) -def get_installed_resource_roots(): - return ( - sublime.installed_packages_path(), - Path(sublime.executable_path()).parent / 'Packages', - ) +class InstalledResourceRoot(ResourceRoot): + def _package_file_path(self, package, *rest): + return self.file_root.joinpath(package + '.sublime-package', *rest) + + def _package_resource_path(self, package, *rest): + package_path = (self.resource_root / package).remove_suffix('.sublime-package') + return package_path.joinpath(*rest) + + +def wrap_path(p): + if isinstance(p, Path): + return p + else: + return Path(p) + + +_ROOTS = None +def get_roots(): + global _ROOTS + if _ROOTS is None: + _ROOTS = [ + DirectoryResourceRoot('Cache', sublime.cache_path()), + DirectoryResourceRoot('Packages', sublime.packages_path()), + InstalledResourceRoot('Packages', sublime.installed_packages_path()), + InstalledResourceRoot('Packages', Path(sublime.executable_path()).parent / 'Packages'), + ] + return _ROOTS class ResourcePath(): @@ -97,30 +181,13 @@ def from_file_path(cls, file_path): ) ResourcePath("Packages/My Package/foo.py") """ - file_path = Path(file_path) - if not file_path.is_absolute(): - raise ValueError("Cannot convert a relative file path to a resource path.") - - for root, base in get_resource_roots().items(): - try: - rel = file_path.relative_to(base) - except ValueError: - pass - else: - return cls(root, *rel.parts) - for base in get_installed_resource_roots(): + file_path = wrap_path(file_path) + for root in get_roots(): try: - rel = file_path.relative_to(base).parts - - if rel == (): - return cls('Packages') - else: - package, *rest = rel - return (cls('Packages', package) - .remove_suffix('.sublime-package').joinpath(*rest)) + return root.file_to_resource_path(file_path) except ValueError: - pass + continue raise ValueError("Path {!r} does not correspond to any resource path.".format(file_path)) @@ -130,13 +197,21 @@ def __init__(self, *pathsegments): :raise ValueError: if the resulting path would be empty. """ - self._parts = tuple( + first, *rest = pathsegments + if isinstance(first, ResourcePath): + self._parts = first.parts + self._parse_segments(rest) + else: + self._parts = self._parse_segments(pathsegments) + + if self._parts == (): + raise ValueError("Empty path.") + + def _parse_segments(self, pathsegments): + return tuple( part for segment in pathsegments if segment for part in posixpath.normpath(str(segment)).split('/') ) - if self._parts == (): - raise ValueError("Empty path.") def __hash__(self): return hash(self.parts) @@ -355,10 +430,13 @@ def file_path(self): :raise ValueError: if the path's root is not used by Sublime. """ - try: - return Path(get_resource_roots()[self.root]).joinpath(*self.parts[1:]) - except KeyError: - raise ValueError("Can't find a filesystem path for {!r}.".format(self.root)) from None + for root in get_roots(): + try: + return root.resource_to_file_path(self) + except ValueError: + continue + + raise ValueError("Can't find a filesystem path for {!r}.".format(self.root)) from None def exists(self): """ @@ -471,7 +549,7 @@ def copytree(self, target, exist_ok=False): .. versionadded:: 1.3 """ - target = Path(target) + target = wrap_path(target) os.makedirs(str(target), exist_ok=exist_ok) From b13695b5d0cf783a529ca319636ae8d643181cd5 Mon Sep 17 00:00:00 2001 From: Thomas Smith Date: Mon, 18 Feb 2019 14:41:47 -0500 Subject: [PATCH 2/3] Improved documentation and removed messageless exception. --- st3/sublime_lib/resource_path.py | 71 +++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 11 deletions(-) diff --git a/st3/sublime_lib/resource_path.py b/st3/sublime_lib/resource_path.py index 245fb0c..90e66a5 100644 --- a/st3/sublime_lib/resource_path.py +++ b/st3/sublime_lib/resource_path.py @@ -3,6 +3,7 @@ import posixpath from collections import OrderedDict import os +from abc import ABCMeta, abstractmethod from .vendor.pathlib.pathlib import Path from ._util.glob import get_glob_matcher @@ -17,6 +18,7 @@ def _abs_parts(path): else: return path.parts + def _file_relative_to(path, base): """ Like Path.relative_to, except: @@ -39,17 +41,26 @@ def _file_relative_to(path, base): compare = cf(child_parts[:n]) if compare != cf(base_parts): - raise ValueError() + return None return child_parts[n:] -class ResourceRoot(): +class ResourceRoot(metaclass=ABCMeta): + """ + Represents a directory containing packages. + """ def __init__(self, root, path): self.resource_root = ResourcePath(root) self.file_root = Path(path) def resource_to_file_path(self, resource_path): + """ + Given a :class:`ResourcePath`, + return the corresponding :class:`Path` within this resource root. + + :raise ValueError: if the :class:`ResourcePath` is not within this resource root. + """ resource_path = ResourcePath(resource_path) parts = resource_path.relative_to(self.resource_root) @@ -59,20 +70,47 @@ def resource_to_file_path(self, resource_path): return self._package_file_path(*parts) def file_to_resource_path(self, file_path): + """ + Given an absolute :class:`Path`, + return the corresponging :class:`ResourcePath` within this resource root, + or ``None`` if there is no such :class:`ResourcePath`. + + :raise ValueError: if the :class:`Path` is relative. + """ file_path = wrap_path(file_path) if not file_path.is_absolute(): raise ValueError("Cannot convert a relative file path to a resource path.") parts = _file_relative_to(file_path, self.file_root) - # parts = file_path.relative_to(self.file_root).parts - if parts == (): + if parts is None: + return None + elif parts == (): return self.resource_root else: return self._package_resource_path(*parts) + @abstractmethod + def _package_file_path(self, package, *parts): + """ + Given a package name and zero or more path segments, + return the corresponding :class:`Path` within this resource root. + """ + ... + + @abstractmethod + def _package_resource_path(self, package, *parts): + """ + Given a package name and zero or more path segments, + return the corresponding :class:`ResourcePath` within this resource root. + """ + ... + class DirectoryResourceRoot(ResourceRoot): + """ + Represents a directory containing unzipped package directories. + """ def _package_file_path(self, *parts): return self.file_root.joinpath(*parts) @@ -81,6 +119,9 @@ def _package_resource_path(self, *parts): class InstalledResourceRoot(ResourceRoot): + """ + Represents a directory containing zipped sublime-package files. + """ def _package_file_path(self, package, *rest): return self.file_root.joinpath(package + '.sublime-package', *rest) @@ -97,6 +138,8 @@ def wrap_path(p): _ROOTS = None + + def get_roots(): global _ROOTS if _ROOTS is None: @@ -183,13 +226,19 @@ def from_file_path(cls, file_path): """ file_path = wrap_path(file_path) - for root in get_roots(): - try: - return root.file_to_resource_path(file_path) - except ValueError: - continue - - raise ValueError("Path {!r} does not correspond to any resource path.".format(file_path)) + try: + return next( + path + for path in ( + root.file_to_resource_path(file_path) + for root in get_roots() + ) + if path is not None + ) + except StopIteration: + raise ValueError( + "Path {!r} does not correspond to any resource path.".format(file_path) + ) def __init__(self, *pathsegments): """ From 432a9f0be828f0017bf22c7859f8c3301e511c3f Mon Sep 17 00:00:00 2001 From: Thomas Smith Date: Mon, 18 Feb 2019 18:55:16 -0500 Subject: [PATCH 3/3] Simplify from_file_path. --- st3/sublime_lib/resource_path.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/st3/sublime_lib/resource_path.py b/st3/sublime_lib/resource_path.py index 90e66a5..41f81f6 100644 --- a/st3/sublime_lib/resource_path.py +++ b/st3/sublime_lib/resource_path.py @@ -226,16 +226,11 @@ def from_file_path(cls, file_path): """ file_path = wrap_path(file_path) - try: - return next( - path - for path in ( - root.file_to_resource_path(file_path) - for root in get_roots() - ) - if path is not None - ) - except StopIteration: + candidates = (root.file_to_resource_path(file_path) for root in get_roots()) + path = next(filter(None, candidates), None) + if path: + return path + else: raise ValueError( "Path {!r} does not correspond to any resource path.".format(file_path) )