Compare commits

...

7 Commits

Author SHA1 Message Date
49844e17b2 Bumped version to 1.19 2021-02-23 11:58:09 +01:00
06432a3534 Mark 1.19 as released in CHANGELOG.md 2021-02-23 11:58:03 +01:00
3a2e9bc672 Simplify @pyside_cache decorator
This fixes a compatibility issue with Python 3.9+, and at the same time
avoids a not-yet-quite-stable area of Blender's Python API.
2021-02-23 11:57:29 +01:00
ce331c7b22 Mark 1.18 as released 2021-02-16 11:58:05 +01:00
8b5dc65d84 Bumped version to 1.18 2021-02-16 11:58:05 +01:00
3bc7dcfa9e Update update_script.sh for new formatting with Black 2021-02-16 11:58:05 +01:00
d9fe24ece7 Cleanup: reformat setup.py with Black
No functional changes.
2021-02-16 11:58:02 +01:00
7 changed files with 115 additions and 106 deletions

View File

@@ -1,6 +1,10 @@
# Blender Cloud changelog # Blender Cloud changelog
## Version 1.18 (in development) ## Version 1.19 (2021-02-23)
- Another Python 3.9+ compatibility fix.
## Version 1.18 (2021-02-16)
- Add compatibility with Python 3.9 (as used in Blender 2.93). - Add compatibility with Python 3.9 (as used in Blender 2.93).
- Drop compatibility with Blender 2.79 and older. The last version of the - Drop compatibility with Blender 2.79 and older. The last version of the

View File

@@ -21,7 +21,7 @@
bl_info = { bl_info = {
"name": "Blender Cloud", "name": "Blender Cloud",
"author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis", "author": "Sybren A. Stüvel, Francesco Siddi, Inês Almeida, Antony Riakiotakis",
"version": (1, 17), "version": (1, 19),
"blender": (2, 80, 0), "blender": (2, 80, 0),
"location": "Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser", "location": "Addon Preferences panel, and Ctrl+Shift+Alt+A anywhere for texture browser",
"description": "Texture library browser and Blender Sync. Requires the Blender ID addon " "description": "Texture library browser and Blender Sync. Requires the Blender ID addon "

View File

@@ -47,7 +47,7 @@ log = logging.getLogger(__name__)
icons = None icons = None
@pyside_cache("version") @pyside_cache
def blender_syncable_versions(self, context): def blender_syncable_versions(self, context):
"""Returns the list of items used by SyncStatusProperties.version EnumProperty.""" """Returns the list of items used by SyncStatusProperties.version EnumProperty."""
@@ -117,7 +117,7 @@ class SyncStatusProperties(PropertyGroup):
self["available_blender_versions"] = new_versions self["available_blender_versions"] = new_versions
@pyside_cache("project") @pyside_cache
def bcloud_available_projects(self, context): def bcloud_available_projects(self, context):
"""Returns the list of items used by BlenderCloudProjectGroup.project EnumProperty.""" """Returns the list of items used by BlenderCloudProjectGroup.project EnumProperty."""

View File

@@ -91,7 +91,7 @@ def scene_sample_count(scene) -> int:
return samples return samples
@pyside_cache("manager") @pyside_cache
def available_managers(self, context): def available_managers(self, context):
"""Returns the list of items used by a manager-selector EnumProperty.""" """Returns the list of items used by a manager-selector EnumProperty."""

View File

@@ -19,6 +19,7 @@
import json import json
import pathlib import pathlib
import typing import typing
from typing import Any, Dict, Optional, Tuple
def sizeof_fmt(num: int, suffix="B") -> str: def sizeof_fmt(num: int, suffix="B") -> str:
@@ -35,7 +36,7 @@ def sizeof_fmt(num: int, suffix="B") -> str:
return "%.1f Yi%s" % (num, suffix) return "%.1f Yi%s" % (num, suffix)
def find_in_path(path: pathlib.Path, filename: str) -> typing.Optional[pathlib.Path]: def find_in_path(path: pathlib.Path, filename: str) -> Optional[pathlib.Path]:
"""Performs a breadth-first search for the filename. """Performs a breadth-first search for the filename.
Returns the path that contains the file, or None if not found. Returns the path that contains the file, or None if not found.
@@ -66,41 +67,29 @@ def find_in_path(path: pathlib.Path, filename: str) -> typing.Optional[pathlib.P
return None return None
def pyside_cache(propname): # Mapping from (module name, function name) to the last value returned by that function.
_pyside_cache: Dict[Tuple[str, str], Any] = {}
def pyside_cache(wrapped):
"""Decorator, stores the result of the decorated callable in Python-managed memory. """Decorator, stores the result of the decorated callable in Python-managed memory.
This is to work around the warning at This is to work around the warning at
https://www.blender.org/api/blender_python_api_master/bpy.props.html#bpy.props.EnumProperty https://www.blender.org/api/blender_python_api_master/bpy.props.html#bpy.props.EnumProperty
""" """
if callable(propname):
raise TypeError('Usage: pyside_cache("property_name")')
def decorator(wrapped):
"""Stores the result of the callable in Python-managed memory.
This is to work around the warning at
https://www.blender.org/api/blender_python_api_master/bpy.props.html#bpy.props.EnumProperty
"""
import functools import functools
@functools.wraps(wrapped) @functools.wraps(wrapped)
# We can't use (*args, **kwargs), because EnumProperty explicitly checks # We can't use (*args, **kwargs), because EnumProperty explicitly checks
# for the number of fixed positional arguments. # for the number of fixed positional arguments.
def wrapper(self, context): def decorator(self, context):
result = None result = None
try: try:
result = wrapped(self, context) result = wrapped(self, context)
return result return result
finally: finally:
try: _pyside_cache[wrapped.__module__, wrapped.__name__] = result
rna_type, rna_info = self.bl_rna.__annotations__[propname]
except AttributeError:
rna_type, rna_info = getattr(self.bl_rna, propname)
rna_info["_cached_result"] = result
return wrapper
return decorator return decorator

156
setup.py
View File

@@ -32,12 +32,14 @@ from distutils.command.install import install, INSTALL_SCHEMES
from distutils.command.install_egg_info import install_egg_info from distutils.command.install_egg_info import install_egg_info
from setuptools import setup, find_packages from setuptools import setup, find_packages
requirement_re = re.compile('[><=]+') requirement_re = re.compile("[><=]+")
sys.dont_write_bytecode = True sys.dont_write_bytecode = True
# Download wheels from pypi. The specific versions are taken from requirements.txt # Download wheels from pypi. The specific versions are taken from requirements.txt
wheels = [ wheels = [
'lockfile', 'pillarsdk', 'blender-asset-tracer', "lockfile",
"pillarsdk",
"blender-asset-tracer",
] ]
@@ -55,9 +57,9 @@ class BuildWheels(Command):
description = "builds/downloads the dependencies as wheel files" description = "builds/downloads the dependencies as wheel files"
user_options = [ user_options = [
('wheels-path=', None, "wheel file installation path"), ("wheels-path=", None, "wheel file installation path"),
('deps-path=', None, "path in which dependencies are built"), ("deps-path=", None, "path in which dependencies are built"),
('cachecontrol-path=', None, "subdir of deps-path containing CacheControl"), ("cachecontrol-path=", None, "subdir of deps-path containing CacheControl"),
] ]
def initialize_options(self): def initialize_options(self):
@@ -70,22 +72,23 @@ class BuildWheels(Command):
self.my_path = pathlib.Path(__file__).resolve().parent self.my_path = pathlib.Path(__file__).resolve().parent
package_path = self.my_path / self.distribution.get_name() package_path = self.my_path / self.distribution.get_name()
self.wheels_path = set_default_path(self.wheels_path, package_path / 'wheels') self.wheels_path = set_default_path(self.wheels_path, package_path / "wheels")
self.deps_path = set_default_path(self.deps_path, self.my_path / 'build/deps') self.deps_path = set_default_path(self.deps_path, self.my_path / "build/deps")
self.cachecontrol_path = set_default_path(self.cachecontrol_path, self.cachecontrol_path = set_default_path(
self.deps_path / 'cachecontrol') self.cachecontrol_path, self.deps_path / "cachecontrol"
self.bat_path = self.deps_path / 'bat' )
self.bat_path = self.deps_path / "bat"
def run(self): def run(self):
log.info('Storing wheels in %s', self.wheels_path) log.info("Storing wheels in %s", self.wheels_path)
# Parse the requirements.txt file # Parse the requirements.txt file
requirements = {} requirements = {}
with open(str(self.my_path / 'requirements.txt')) as reqfile: with open(str(self.my_path / "requirements.txt")) as reqfile:
for line in reqfile.readlines(): for line in reqfile.readlines():
line = line.strip() line = line.strip()
if not line or line.startswith('#'): if not line or line.startswith("#"):
# comments are lines that start with # only # comments are lines that start with # only
continue continue
@@ -97,37 +100,45 @@ class BuildWheels(Command):
self.wheels_path.mkdir(parents=True, exist_ok=True) self.wheels_path.mkdir(parents=True, exist_ok=True)
for package in wheels: for package in wheels:
pattern = package.replace('-', '_') + '*.whl' pattern = package.replace("-", "_") + "*.whl"
if list(self.wheels_path.glob(pattern)): if list(self.wheels_path.glob(pattern)):
continue continue
self.download_wheel(requirements[package]) self.download_wheel(requirements[package])
# Build CacheControl. # Build CacheControl.
if not list(self.wheels_path.glob('CacheControl*.whl')): if not list(self.wheels_path.glob("CacheControl*.whl")):
log.info('Building CacheControl in %s', self.cachecontrol_path) log.info("Building CacheControl in %s", self.cachecontrol_path)
# self.git_clone(self.cachecontrol_path, # self.git_clone(self.cachecontrol_path,
# 'https://github.com/ionrock/cachecontrol.git', # 'https://github.com/ionrock/cachecontrol.git',
# 'v%s' % requirements['CacheControl'][1]) # 'v%s' % requirements['CacheControl'][1])
# FIXME: we need my clone until pull request #125 has been merged & released # FIXME: we need my clone until pull request #125 has been merged & released
self.git_clone(self.cachecontrol_path, self.git_clone(
'https://github.com/sybrenstuvel/cachecontrol.git', self.cachecontrol_path,
'sybren-filecache-delete-crash-fix') "https://github.com/sybrenstuvel/cachecontrol.git",
"sybren-filecache-delete-crash-fix",
)
self.build_copy_wheel(self.cachecontrol_path) self.build_copy_wheel(self.cachecontrol_path)
# Ensure that the wheels are added to the data files. # Ensure that the wheels are added to the data files.
self.distribution.data_files.append( self.distribution.data_files.append(
('blender_cloud/wheels', (str(p) for p in self.wheels_path.glob('*.whl'))) ("blender_cloud/wheels", (str(p) for p in self.wheels_path.glob("*.whl")))
) )
def download_wheel(self, requirement): def download_wheel(self, requirement):
"""Downloads a wheel from PyPI and saves it in self.wheels_path.""" """Downloads a wheel from PyPI and saves it in self.wheels_path."""
subprocess.check_call([ subprocess.check_call(
sys.executable, '-m', 'pip', [
'download', '--no-deps', sys.executable,
'--dest', str(self.wheels_path), "-m",
requirement[0] "pip",
]) "download",
"--no-deps",
"--dest",
str(self.wheels_path),
requirement[0],
]
)
def git_clone(self, workdir: pathlib.Path, git_url: str, checkout: str = None): def git_clone(self, workdir: pathlib.Path, git_url: str, checkout: str = None):
if workdir.exists(): if workdir.exists():
@@ -136,24 +147,25 @@ class BuildWheels(Command):
workdir.mkdir(parents=True) workdir.mkdir(parents=True)
subprocess.check_call(['git', 'clone', git_url, str(workdir)], subprocess.check_call(
cwd=str(workdir.parent)) ["git", "clone", git_url, str(workdir)], cwd=str(workdir.parent)
)
if checkout: if checkout:
subprocess.check_call(['git', 'checkout', checkout], subprocess.check_call(["git", "checkout", checkout], cwd=str(workdir))
cwd=str(workdir))
def build_copy_wheel(self, package_path: pathlib.Path): def build_copy_wheel(self, package_path: pathlib.Path):
# Make sure no wheels exist yet, so that we know which one to copy later. # Make sure no wheels exist yet, so that we know which one to copy later.
to_remove = list((package_path / 'dist').glob('*.whl')) to_remove = list((package_path / "dist").glob("*.whl"))
for fname in to_remove: for fname in to_remove:
fname.unlink() fname.unlink()
subprocess.check_call([sys.executable, 'setup.py', 'bdist_wheel'], subprocess.check_call(
cwd=str(package_path)) [sys.executable, "setup.py", "bdist_wheel"], cwd=str(package_path)
)
wheel = next((package_path / 'dist').glob('*.whl')) wheel = next((package_path / "dist").glob("*.whl"))
log.info('copying %s to %s', wheel, self.wheels_path) log.info("copying %s to %s", wheel, self.wheels_path)
shutil.copy(str(wheel), str(self.wheels_path)) shutil.copy(str(wheel), str(self.wheels_path))
@@ -163,19 +175,19 @@ class BlenderAddonBdist(bdist):
def initialize_options(self): def initialize_options(self):
super().initialize_options() super().initialize_options()
self.formats = ['zip'] self.formats = ["zip"]
self.plat_name = 'addon' # use this instead of 'linux-x86_64' or similar. self.plat_name = "addon" # use this instead of 'linux-x86_64' or similar.
self.fix_local_prefix() self.fix_local_prefix()
def fix_local_prefix(self): def fix_local_prefix(self):
"""Place data files in blender_cloud instead of local/blender_cloud.""" """Place data files in blender_cloud instead of local/blender_cloud."""
for key in INSTALL_SCHEMES: for key in INSTALL_SCHEMES:
if 'data' not in INSTALL_SCHEMES[key]: if "data" not in INSTALL_SCHEMES[key]:
continue continue
INSTALL_SCHEMES[key]['data'] = '$base' INSTALL_SCHEMES[key]["data"] = "$base"
def run(self): def run(self):
self.run_command('wheels') self.run_command("wheels")
super().run() super().run()
@@ -184,7 +196,7 @@ class BlenderAddonFdist(BlenderAddonBdist):
"""Ensures that 'python setup.py fdist' creates a plain folder structure.""" """Ensures that 'python setup.py fdist' creates a plain folder structure."""
user_options = [ user_options = [
('dest-path=', None, 'addon installation path'), ("dest-path=", None, "addon installation path"),
] ]
def initialize_options(self): def initialize_options(self):
@@ -198,12 +210,12 @@ class BlenderAddonFdist(BlenderAddonBdist):
filepath = self.distribution.dist_files[0][2] filepath = self.distribution.dist_files[0][2]
# if dest_path is not specified use the filename as the dest_path (minus the .zip) # if dest_path is not specified use the filename as the dest_path (minus the .zip)
assert filepath.endswith('.zip') assert filepath.endswith(".zip")
target_folder = self.dest_path or filepath[:-4] target_folder = self.dest_path or filepath[:-4]
print('Unzipping the package on {}.'.format(target_folder)) print("Unzipping the package on {}.".format(target_folder))
with zipfile.ZipFile(filepath, 'r') as zip_ref: with zipfile.ZipFile(filepath, "r") as zip_ref:
zip_ref.extractall(target_folder) zip_ref.extractall(target_folder)
@@ -213,8 +225,8 @@ class BlenderAddonInstall(install):
def initialize_options(self): def initialize_options(self):
super().initialize_options() super().initialize_options()
self.prefix = '' self.prefix = ""
self.install_lib = '' self.install_lib = ""
class AvoidEggInfo(install_egg_info): class AvoidEggInfo(install_egg_info):
@@ -229,34 +241,38 @@ class AvoidEggInfo(install_egg_info):
setup( setup(
cmdclass={'bdist': BlenderAddonBdist, cmdclass={
'fdist': BlenderAddonFdist, "bdist": BlenderAddonBdist,
'install': BlenderAddonInstall, "fdist": BlenderAddonFdist,
'install_egg_info': AvoidEggInfo, "install": BlenderAddonInstall,
'wheels': BuildWheels}, "install_egg_info": AvoidEggInfo,
name='blender_cloud', "wheels": BuildWheels,
description='The Blender Cloud addon allows browsing the Blender Cloud from Blender.', },
version='1.17', name="blender_cloud",
author='Sybren A. Stüvel', description="The Blender Cloud addon allows browsing the Blender Cloud from Blender.",
author_email='sybren@stuvel.eu', version="1.19",
packages=find_packages('.'), author="Sybren A. Stüvel",
author_email="sybren@stuvel.eu",
packages=find_packages("."),
data_files=[ data_files=[
('blender_cloud', ['README.md', 'README-flamenco.md', 'CHANGELOG.md']), ("blender_cloud", ["README.md", "README-flamenco.md", "CHANGELOG.md"]),
('blender_cloud/icons', glob.glob('blender_cloud/icons/*')), ("blender_cloud/icons", glob.glob("blender_cloud/icons/*")),
('blender_cloud/texture_browser/icons', (
glob.glob('blender_cloud/texture_browser/icons/*')) "blender_cloud/texture_browser/icons",
glob.glob("blender_cloud/texture_browser/icons/*"),
),
], ],
scripts=[], scripts=[],
url='https://developer.blender.org/diffusion/BCA/', url="https://developer.blender.org/diffusion/BCA/",
license='GNU General Public License v2 or later (GPLv2+)', license="GNU General Public License v2 or later (GPLv2+)",
platforms='', platforms="",
classifiers=[ classifiers=[
'Intended Audience :: End Users/Desktop', "Intended Audience :: End Users/Desktop",
'Operating System :: OS Independent', "Operating System :: OS Independent",
'Environment :: Plugins', "Environment :: Plugins",
'License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)', "License :: OSI Approved :: GNU General Public License v2 or later (GPLv2+)",
'Programming Language :: Python', "Programming Language :: Python",
'Programming Language :: Python :: 3.5', "Programming Language :: Python :: 3.5",
], ],
zip_safe=False, zip_safe=False,
) )

View File

@@ -9,8 +9,8 @@ fi
BL_INFO_VER=$(echo "$VERSION" | sed 's/\./, /g') BL_INFO_VER=$(echo "$VERSION" | sed 's/\./, /g')
sed "s/version='[^']*'/version='$VERSION'/" -i setup.py sed "s/version=\"[^\"]*\"/version=\"$VERSION\"/" -i setup.py
sed "s/'version': ([^)]*)/'version': ($BL_INFO_VER)/" -i blender_cloud/__init__.py sed "s/\"version\": ([^)]*)/\"version\": ($BL_INFO_VER)/" -i blender_cloud/__init__.py
git diff git diff
echo echo