Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 165 additions & 43 deletions st3/sublime_lib/resource_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -11,18 +12,144 @@
__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 get_installed_resource_roots():
return (
sublime.installed_packages_path(),
Path(sublime.executable_path()).parent / 'Packages',
)
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):
return None

return child_parts[n:]


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)
if parts == ():
return self.file_root
else:
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)
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)

def _package_resource_path(self, *parts):
return self.resource_root.joinpath(*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)

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():
Expand Down Expand Up @@ -97,46 +224,38 @@ 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():
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))
except ValueError:
pass

raise ValueError("Path {!r} does not correspond to any resource path.".format(file_path))
file_path = wrap_path(file_path)
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)
)

def __init__(self, *pathsegments):
"""
Construct a :class:`ResourcePath` object with the given parts.

: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)
Expand Down Expand Up @@ -355,10 +474,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):
"""
Expand Down Expand Up @@ -471,7 +593,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)

Expand Down