2021-10-18 15:54:04 +02:00
|
|
|
# ***** BEGIN GPL LICENSE BLOCK *****
|
|
|
|
#
|
|
|
|
# This program 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 2
|
|
|
|
# of the License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# 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, write to the Free Software Foundation,
|
|
|
|
# Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
|
|
|
|
#
|
|
|
|
# ***** END GPL LICENCE BLOCK *****
|
|
|
|
#
|
|
|
|
# (c) 2018, Blender Foundation - Sybren A. Stüvel
|
|
|
|
"""Blender path support.
|
|
|
|
|
|
|
|
Does not use pathlib, because we may have to handle POSIX paths on Windows
|
|
|
|
or vice versa.
|
|
|
|
"""
|
|
|
|
|
|
|
|
import os.path
|
|
|
|
import pathlib
|
|
|
|
import platform
|
|
|
|
import string
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
|
|
class BlendPath(bytes):
|
|
|
|
"""A path within Blender is always stored as bytes."""
|
|
|
|
|
|
|
|
def __new__(cls, path):
|
|
|
|
if isinstance(path, pathlib.PurePath):
|
2023-01-10 11:41:55 +01:00
|
|
|
path = str(path).encode("utf-8")
|
2021-10-18 15:54:04 +02:00
|
|
|
if not isinstance(path, bytes):
|
2023-01-10 11:41:55 +01:00
|
|
|
raise TypeError("path must be bytes or pathlib.Path, but is %r" % path)
|
2021-10-18 15:54:04 +02:00
|
|
|
|
2023-01-10 11:41:55 +01:00
|
|
|
return super().__new__(cls, path.replace(b"\\", b"/"))
|
2021-10-18 15:54:04 +02:00
|
|
|
|
|
|
|
@classmethod
|
2023-01-10 11:41:55 +01:00
|
|
|
def mkrelative(
|
|
|
|
cls, asset_path: pathlib.PurePath, bfile_path: pathlib.PurePath
|
|
|
|
) -> "BlendPath":
|
2021-10-18 15:54:04 +02:00
|
|
|
"""Construct a BlendPath to the asset relative to the blend file.
|
|
|
|
|
|
|
|
Assumes that bfile_path is absolute.
|
|
|
|
|
|
|
|
Note that this can return an absolute path on Windows when 'asset_path'
|
|
|
|
and 'bfile_path' are on different drives.
|
|
|
|
"""
|
|
|
|
from collections import deque
|
|
|
|
|
|
|
|
# Only compare absolute paths.
|
2023-01-10 11:41:55 +01:00
|
|
|
assert bfile_path.is_absolute(), (
|
|
|
|
"BlendPath().mkrelative(bfile_path=%r) should get absolute bfile_path"
|
|
|
|
% bfile_path
|
|
|
|
)
|
|
|
|
assert asset_path.is_absolute(), (
|
|
|
|
"BlendPath().mkrelative(asset_path=%r) should get absolute asset_path"
|
|
|
|
% asset_path
|
|
|
|
)
|
2021-10-18 15:54:04 +02:00
|
|
|
|
|
|
|
# There is no way to construct a relative path between drives.
|
|
|
|
if bfile_path.drive != asset_path.drive:
|
|
|
|
return cls(asset_path)
|
|
|
|
|
|
|
|
bdir_parts = deque(bfile_path.parent.parts)
|
|
|
|
asset_path = make_absolute(asset_path)
|
|
|
|
asset_parts = deque(asset_path.parts)
|
|
|
|
|
|
|
|
# Remove matching initial parts. What is left in bdir_parts represents
|
|
|
|
# the number of '..' we need. What is left in asset_parts represents
|
|
|
|
# what we need after the '../../../'.
|
|
|
|
while bdir_parts:
|
|
|
|
if bdir_parts[0] != asset_parts[0]:
|
|
|
|
break
|
|
|
|
bdir_parts.popleft()
|
|
|
|
asset_parts.popleft()
|
|
|
|
|
|
|
|
rel_asset = pathlib.PurePath(*asset_parts)
|
|
|
|
# TODO(Sybren): should we use sys.getfilesystemencoding() instead?
|
2023-01-10 11:41:55 +01:00
|
|
|
rel_bytes = str(rel_asset).encode("utf-8")
|
|
|
|
as_bytes = b"//" + len(bdir_parts) * b"../" + rel_bytes
|
2021-10-18 15:54:04 +02:00
|
|
|
return cls(as_bytes)
|
|
|
|
|
|
|
|
def __str__(self) -> str:
|
|
|
|
"""Decodes the path as UTF-8, replacing undecodable bytes.
|
|
|
|
|
|
|
|
Undecodable bytes are ignored so this function can be safely used
|
|
|
|
for reporting.
|
|
|
|
"""
|
2023-01-10 11:41:55 +01:00
|
|
|
return self.decode("utf8", errors="replace")
|
2021-10-18 15:54:04 +02:00
|
|
|
|
|
|
|
def __repr__(self) -> str:
|
2023-01-10 11:41:55 +01:00
|
|
|
return "BlendPath(%s)" % super().__repr__()
|
2021-10-18 15:54:04 +02:00
|
|
|
|
|
|
|
def __truediv__(self, subpath: bytes):
|
|
|
|
"""Slash notation like pathlib.Path."""
|
|
|
|
sub = BlendPath(subpath)
|
|
|
|
if sub.is_absolute():
|
|
|
|
raise ValueError("'a / b' only works when 'b' is a relative path")
|
2023-01-10 11:41:55 +01:00
|
|
|
return BlendPath(self.rstrip(b"/") + b"/" + sub)
|
2021-10-18 15:54:04 +02:00
|
|
|
|
|
|
|
def __rtruediv__(self, parentpath: bytes):
|
|
|
|
"""Slash notation like pathlib.Path."""
|
|
|
|
if self.is_absolute():
|
|
|
|
raise ValueError("'a / b' only works when 'b' is a relative path")
|
2023-01-10 11:41:55 +01:00
|
|
|
return BlendPath(parentpath.rstrip(b"/") + b"/" + self)
|
2021-10-18 15:54:04 +02:00
|
|
|
|
|
|
|
def to_path(self) -> pathlib.PurePath:
|
|
|
|
"""Convert this path to a pathlib.PurePath.
|
|
|
|
|
|
|
|
This path MUST NOT be a blendfile-relative path (e.g. it may not start
|
|
|
|
with `//`). For such paths, first use `.absolute()` to resolve the path.
|
|
|
|
|
|
|
|
Interprets the path as UTF-8, and if that fails falls back to the local
|
|
|
|
filesystem encoding.
|
|
|
|
|
|
|
|
The exact type returned is determined by the current platform.
|
|
|
|
"""
|
|
|
|
# TODO(Sybren): once we target Python 3.6, implement __fspath__().
|
|
|
|
try:
|
2023-01-10 11:41:55 +01:00
|
|
|
decoded = self.decode("utf8")
|
2021-10-18 15:54:04 +02:00
|
|
|
except UnicodeDecodeError:
|
|
|
|
decoded = self.decode(sys.getfilesystemencoding())
|
|
|
|
if self.is_blendfile_relative():
|
2023-01-10 11:41:55 +01:00
|
|
|
raise ValueError("to_path() cannot be used on blendfile-relative paths")
|
2021-10-18 15:54:04 +02:00
|
|
|
return pathlib.PurePath(decoded)
|
|
|
|
|
|
|
|
def is_blendfile_relative(self) -> bool:
|
2023-01-10 11:41:55 +01:00
|
|
|
return self[:2] == b"//"
|
2021-10-18 15:54:04 +02:00
|
|
|
|
|
|
|
def is_absolute(self) -> bool:
|
|
|
|
if self.is_blendfile_relative():
|
|
|
|
return False
|
2023-01-10 11:41:55 +01:00
|
|
|
if self[0:1] == b"/":
|
2021-10-18 15:54:04 +02:00
|
|
|
return True
|
|
|
|
|
|
|
|
# Windows style path starting with drive letter.
|
2023-01-10 11:41:55 +01:00
|
|
|
if (
|
|
|
|
len(self) >= 3
|
|
|
|
and (self.decode("utf8"))[0] in string.ascii_letters
|
|
|
|
and self[1:2] == b":"
|
|
|
|
and self[2:3] in {b"\\", b"/"}
|
|
|
|
):
|
2021-10-18 15:54:04 +02:00
|
|
|
return True
|
|
|
|
|
|
|
|
return False
|
|
|
|
|
2023-01-10 11:41:55 +01:00
|
|
|
def absolute(self, root: bytes = b"") -> "BlendPath":
|
2021-10-18 15:54:04 +02:00
|
|
|
"""Determine absolute path.
|
|
|
|
|
|
|
|
:param root: root directory to compute paths relative to.
|
|
|
|
For blendfile-relative paths, root should be the directory
|
|
|
|
containing the blendfile. If not given, blendfile-relative
|
|
|
|
paths cause a ValueError but filesystem-relative paths are
|
|
|
|
resolved based on the current working directory.
|
|
|
|
"""
|
|
|
|
if self.is_absolute():
|
|
|
|
return self
|
|
|
|
|
|
|
|
if self.is_blendfile_relative():
|
|
|
|
my_relpath = self[2:] # strip off leading //
|
|
|
|
else:
|
|
|
|
my_relpath = self
|
|
|
|
return BlendPath(os.path.join(root, my_relpath))
|
|
|
|
|
|
|
|
|
|
|
|
def make_absolute(path: pathlib.PurePath) -> pathlib.Path:
|
|
|
|
"""Make the path absolute without resolving symlinks or drive letters.
|
|
|
|
|
|
|
|
This function is an alternative to `Path.resolve()`. It make the path absolute,
|
|
|
|
and resolves `../../`, but contrary to `Path.resolve()` does NOT perform these
|
|
|
|
changes:
|
|
|
|
- Symlinks are NOT followed.
|
|
|
|
- Windows Network shares that are mapped to a drive letter are NOT resolved
|
|
|
|
to their UNC notation.
|
|
|
|
|
|
|
|
The type of the returned path is determined by the current platform.
|
|
|
|
"""
|
|
|
|
str_path = path.as_posix()
|
2023-01-10 11:41:55 +01:00
|
|
|
if len(str_path) >= 2 and str_path[0].isalpha() and str_path[1] == ":":
|
2021-10-18 15:54:04 +02:00
|
|
|
# This is an absolute Windows path. It must be handled with care on non-Windows platforms.
|
2023-01-10 11:41:55 +01:00
|
|
|
if platform.system() != "Windows":
|
2021-10-18 15:54:04 +02:00
|
|
|
# Normalize the POSIX-like part of the path, but leave out the drive letter.
|
|
|
|
non_drive_path = str_path[2:]
|
|
|
|
normalized = os.path.normpath(non_drive_path)
|
|
|
|
# Stick the drive letter back on the normalized path.
|
|
|
|
return pathlib.Path(str_path[:2] + normalized)
|
|
|
|
|
|
|
|
return pathlib.Path(os.path.abspath(str_path))
|
|
|
|
|
|
|
|
|
|
|
|
def strip_root(path: pathlib.PurePath) -> pathlib.PurePosixPath:
|
|
|
|
"""Turn the path into a relative path by stripping the root.
|
|
|
|
|
|
|
|
This also turns any drive letter into a normal path component.
|
|
|
|
|
|
|
|
This changes "C:/Program Files/Blender" to "C/Program Files/Blender",
|
|
|
|
and "/absolute/path.txt" to "absolute/path.txt", making it possible to
|
|
|
|
treat it as a relative path.
|
|
|
|
"""
|
|
|
|
|
|
|
|
if path.drive:
|
|
|
|
return pathlib.PurePosixPath(path.drive[0], *path.parts[1:])
|
|
|
|
if isinstance(path, pathlib.PurePosixPath):
|
|
|
|
# This happens when running on POSIX but still handling paths
|
|
|
|
# originating from a Windows machine.
|
|
|
|
parts = path.parts
|
2023-01-10 11:41:55 +01:00
|
|
|
if (
|
|
|
|
parts
|
|
|
|
and len(parts[0]) == 2
|
|
|
|
and parts[0][0].isalpha()
|
|
|
|
and parts[0][1] == ":"
|
|
|
|
):
|
2021-10-18 15:54:04 +02:00
|
|
|
# The first part is a drive letter.
|
|
|
|
return pathlib.PurePosixPath(parts[0][0], *path.parts[1:])
|
|
|
|
|
|
|
|
if path.is_absolute():
|
|
|
|
return pathlib.PurePosixPath(*path.parts[1:])
|
|
|
|
return pathlib.PurePosixPath(path)
|