feat: Better exceptions and parsing.
parent
8ba4302b64
commit
8220491d50
124
blext/cli.py
124
blext/cli.py
|
@ -19,12 +19,13 @@
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import typing as typ
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import cyclopts
|
import cyclopts
|
||||||
|
import pydantic as pyd
|
||||||
import rich
|
import rich
|
||||||
|
|
||||||
|
from . import exceptions as exc
|
||||||
from . import finders, pack, spec, supported, wheels
|
from . import finders, pack, spec, supported, wheels
|
||||||
|
|
||||||
console = rich.console.Console()
|
console = rich.console.Console()
|
||||||
|
@ -42,6 +43,39 @@ PATH_BL_INIT_PY: Path = (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _parse(
|
||||||
|
*,
|
||||||
|
bl_platform: supported.BLPlatform | None = None,
|
||||||
|
proj_path: Path | None = None,
|
||||||
|
release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release,
|
||||||
|
detect_bl_platform: bool = True,
|
||||||
|
) -> tuple[supported.BLPlatform | None, spec.BLExtSpec]:
|
||||||
|
# Parse CLI
|
||||||
|
with exc.handle(exc.pretty, ValueError):
|
||||||
|
path_proj_spec = finders.find_proj_spec(proj_path)
|
||||||
|
|
||||||
|
# Parse Blender Extension Specification
|
||||||
|
with exc.handle(exc.pretty, ValueError, pyd.ValidationError):
|
||||||
|
universal_blext_spec = spec.BLExtSpec.from_proj_spec(
|
||||||
|
path_proj_spec=path_proj_spec,
|
||||||
|
release_profile=release_profile,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Deduce BLPlatform
|
||||||
|
if bl_platform is not None:
|
||||||
|
_bl_platform = bl_platform
|
||||||
|
elif bl_platform is None and detect_bl_platform:
|
||||||
|
with exc.handle(exc.pretty, ValueError):
|
||||||
|
_bl_platform = finders.detect_local_blplatform()
|
||||||
|
else:
|
||||||
|
return (None, universal_blext_spec)
|
||||||
|
|
||||||
|
return (
|
||||||
|
_bl_platform,
|
||||||
|
universal_blext_spec.constrain_to_bl_platform(_bl_platform),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Command: Build
|
# - Command: Build
|
||||||
####################
|
####################
|
||||||
|
@ -50,8 +84,7 @@ def build(
|
||||||
bl_platform: supported.BLPlatform | None = None,
|
bl_platform: supported.BLPlatform | None = None,
|
||||||
proj_path: Path | None = None,
|
proj_path: Path | None = None,
|
||||||
release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release,
|
release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release,
|
||||||
_return_blext_spec: typ.Annotated[bool, cyclopts.Parameter(show=False)] = False,
|
) -> None:
|
||||||
) -> spec.BLExtSpec | None:
|
|
||||||
"""[Build] the extension to an installable `.zip` file.
|
"""[Build] the extension to an installable `.zip` file.
|
||||||
|
|
||||||
Parameters:
|
Parameters:
|
||||||
|
@ -60,17 +93,14 @@ def build(
|
||||||
release_profile: The release profile to bake into the extension.
|
release_profile: The release profile to bake into the extension.
|
||||||
"""
|
"""
|
||||||
# Parse CLI
|
# Parse CLI
|
||||||
if bl_platform is None:
|
bl_platform, blext_spec = _parse(
|
||||||
bl_platform = finders.detect_local_blplatform()
|
bl_platform=bl_platform,
|
||||||
path_proj_spec = finders.find_proj_spec(proj_path)
|
proj_path=proj_path,
|
||||||
|
|
||||||
# Parse Blender Extension Specification
|
|
||||||
blext_spec = spec.BLExtSpec.from_proj_spec(
|
|
||||||
path_proj_spec=path_proj_spec,
|
|
||||||
release_profile=release_profile,
|
release_profile=release_profile,
|
||||||
).constrain_to_bl_platform(bl_platform)
|
)
|
||||||
|
|
||||||
# Download Wheels
|
# Download Wheels
|
||||||
|
with exc.handle(exc.pretty, ValueError):
|
||||||
wheels.download_wheels(
|
wheels.download_wheels(
|
||||||
blext_spec,
|
blext_spec,
|
||||||
bl_platform=bl_platform,
|
bl_platform=bl_platform,
|
||||||
|
@ -78,6 +108,7 @@ def build(
|
||||||
)
|
)
|
||||||
|
|
||||||
# Pack the Blender Extension
|
# Pack the Blender Extension
|
||||||
|
with exc.handle(exc.pretty, ValueError):
|
||||||
pack.pack_bl_extension(blext_spec, overwrite=True)
|
pack.pack_bl_extension(blext_spec, overwrite=True)
|
||||||
|
|
||||||
# Validate the Blender Extension
|
# Validate the Blender Extension
|
||||||
|
@ -110,12 +141,11 @@ def build(
|
||||||
if return_code == 0:
|
if return_code == 0:
|
||||||
console.print('[✔] Blender Extension Validation Succeeded')
|
console.print('[✔] Blender Extension Validation Succeeded')
|
||||||
else:
|
else:
|
||||||
msg = 'Packed extension did not validate'
|
with exc.handle(exc.pretty, ValueError):
|
||||||
raise ValueError(msg)
|
msgs = [
|
||||||
|
'Blender failed to validate the packed extension. For more information, see the validation logs (above).',
|
||||||
if _return_blext_spec:
|
]
|
||||||
return blext_spec
|
raise ValueError(*msgs)
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
|
@ -126,15 +156,23 @@ def dev(
|
||||||
proj_path: Path | None = None,
|
proj_path: Path | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Launch the local [dev] extension w/Blender."""
|
"""Launch the local [dev] extension w/Blender."""
|
||||||
|
# Parse CLI
|
||||||
|
release_profile = supported.ReleaseProfile.Dev
|
||||||
|
bl_platform, blext_spec = _parse(
|
||||||
|
bl_platform=None,
|
||||||
|
proj_path=proj_path,
|
||||||
|
release_profile=release_profile,
|
||||||
|
)
|
||||||
|
|
||||||
# Build the Extension
|
# Build the Extension
|
||||||
blext_spec = build(
|
build(
|
||||||
bl_platform=None,
|
bl_platform=None,
|
||||||
proj_path=proj_path,
|
proj_path=proj_path,
|
||||||
release_profile=supported.ReleaseProfile.Dev,
|
release_profile=supported.ReleaseProfile.Dev,
|
||||||
_return_blext_spec=True,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find Blender
|
# Find Blender
|
||||||
|
with exc.handle(exc.pretty, ValueError):
|
||||||
blender_exe = finders.find_blender_exe()
|
blender_exe = finders.find_blender_exe()
|
||||||
|
|
||||||
# Launch Blender
|
# Launch Blender
|
||||||
|
@ -188,17 +226,14 @@ def show_spec(
|
||||||
proj_path: Path to a `pyproject.toml` or a folder containing a `pyproject.toml`, which specifies the Blender extension.
|
proj_path: Path to a `pyproject.toml` or a folder containing a `pyproject.toml`, which specifies the Blender extension.
|
||||||
release_profile: The release profile to bake into the extension.
|
release_profile: The release profile to bake into the extension.
|
||||||
"""
|
"""
|
||||||
# Parse BLExtSpec
|
# Parse CLI
|
||||||
path_proj_spec = finders.find_proj_spec(proj_path)
|
_, blext_spec = _parse(
|
||||||
blext_spec = spec.BLExtSpec.from_proj_spec(
|
bl_platform=bl_platform,
|
||||||
path_proj_spec=path_proj_spec,
|
proj_path=proj_path,
|
||||||
release_profile=release_profile,
|
release_profile=release_profile,
|
||||||
|
detect_bl_platform=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Constrain BLExtSpec to BLPlatform
|
|
||||||
if bl_platform is not None:
|
|
||||||
blext_spec = blext_spec.constrain_to_bl_platform(bl_platform)
|
|
||||||
|
|
||||||
# Show BLExtSpec
|
# Show BLExtSpec
|
||||||
rich.print(blext_spec)
|
rich.print(blext_spec)
|
||||||
|
|
||||||
|
@ -206,8 +241,8 @@ def show_spec(
|
||||||
####################
|
####################
|
||||||
# - Command: Show Manifest
|
# - Command: Show Manifest
|
||||||
####################
|
####################
|
||||||
@app_show.command(name='bl_manifest', group='Information')
|
@app_show.command(name='manifest', group='Information')
|
||||||
def show_bl_manifest(
|
def show_manifest(
|
||||||
bl_platform: supported.BLPlatform | None = None,
|
bl_platform: supported.BLPlatform | None = None,
|
||||||
proj_path: Path | None = None,
|
proj_path: Path | None = None,
|
||||||
release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release,
|
release_profile: supported.ReleaseProfile = supported.ReleaseProfile.Release,
|
||||||
|
@ -220,20 +255,13 @@ def show_bl_manifest(
|
||||||
release_profile: The release profile to bake into the extension.
|
release_profile: The release profile to bake into the extension.
|
||||||
"""
|
"""
|
||||||
# Parse CLI
|
# Parse CLI
|
||||||
if bl_platform is None:
|
_, blext_spec = _parse(
|
||||||
bl_platform = finders.detect_local_blplatform()
|
bl_platform=bl_platform,
|
||||||
path_proj_spec = finders.find_proj_spec(proj_path)
|
proj_path=proj_path,
|
||||||
|
|
||||||
# Parse Blender Extension Specification
|
|
||||||
blext_spec = spec.BLExtSpec.from_proj_spec(
|
|
||||||
path_proj_spec=path_proj_spec,
|
|
||||||
release_profile=release_profile,
|
release_profile=release_profile,
|
||||||
|
detect_bl_platform=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# [Maybe] Constrain BLExtSpec to BLPlatform
|
|
||||||
if bl_platform is not None:
|
|
||||||
blext_spec = blext_spec.constrain_to_bl_platform(bl_platform)
|
|
||||||
|
|
||||||
# Show BLExtSpec
|
# Show BLExtSpec
|
||||||
rich.print(blext_spec.manifest_str)
|
rich.print(blext_spec.manifest_str)
|
||||||
|
|
||||||
|
@ -252,20 +280,13 @@ def show_init_settings(
|
||||||
release_profile: The release profile to bake into the extension.
|
release_profile: The release profile to bake into the extension.
|
||||||
"""
|
"""
|
||||||
# Parse CLI
|
# Parse CLI
|
||||||
if bl_platform is None:
|
_, blext_spec = _parse(
|
||||||
bl_platform = finders.detect_local_blplatform()
|
bl_platform=bl_platform,
|
||||||
path_proj_spec = finders.find_proj_spec(proj_path)
|
proj_path=proj_path,
|
||||||
|
|
||||||
# Parse Blender Extension Specification
|
|
||||||
blext_spec = spec.BLExtSpec.from_proj_spec(
|
|
||||||
path_proj_spec=path_proj_spec,
|
|
||||||
release_profile=release_profile,
|
release_profile=release_profile,
|
||||||
|
detect_bl_platform=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# [Maybe] Constrain BLExtSpec to BLPlatform
|
|
||||||
if bl_platform is not None:
|
|
||||||
blext_spec = blext_spec.constrain_to_bl_platform(bl_platform)
|
|
||||||
|
|
||||||
# Show BLExtSpec
|
# Show BLExtSpec
|
||||||
rich.print(blext_spec.init_settings_str)
|
rich.print(blext_spec.init_settings_str)
|
||||||
|
|
||||||
|
@ -276,6 +297,7 @@ def show_init_settings(
|
||||||
@app_show.command(name='path_blender', group='Paths')
|
@app_show.command(name='path_blender', group='Paths')
|
||||||
def show_path_blender() -> None:
|
def show_path_blender() -> None:
|
||||||
"""Display the found path to the Blender executable."""
|
"""Display the found path to the Blender executable."""
|
||||||
|
with exc.handle(exc.pretty, ValueError):
|
||||||
blender_exe = finders.find_blender_exe()
|
blender_exe = finders.find_blender_exe()
|
||||||
|
|
||||||
rich.print(blender_exe)
|
rich.print(blender_exe)
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
# blext
|
||||||
|
# Copyright (C) 2025 blext Project Contributors
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
"""Exception handling for enhancing the end-user experience of dealing with errors."""
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import rich
|
||||||
|
import rich.markdown
|
||||||
|
import rich.padding
|
||||||
|
import pydantic as pyd
|
||||||
|
|
||||||
|
|
||||||
|
def sys_exit_1():
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def handle(
|
||||||
|
ex_handler,
|
||||||
|
*exceptions,
|
||||||
|
after_ex=sys_exit_1,
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
yield
|
||||||
|
except exceptions as ex:
|
||||||
|
ex_handler(ex)
|
||||||
|
after_ex()
|
||||||
|
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Exception Handlers
|
||||||
|
####################
|
||||||
|
def pretty(ex: Exception, show_filename_lineno: bool = False) -> None:
|
||||||
|
# Parse ValidationError
|
||||||
|
if isinstance(ex, pyd.ValidationError):
|
||||||
|
model_name = ex.title
|
||||||
|
ex_name = f'ValidationError{"s" if ex.error_count() > 1 else ""}'
|
||||||
|
|
||||||
|
ex = ValueError(
|
||||||
|
*[
|
||||||
|
f'{model_name}.'
|
||||||
|
+ ', '.join([str(el) for el in err['loc']])
|
||||||
|
+ '='
|
||||||
|
+ str(err['input'])
|
||||||
|
+ ': '
|
||||||
|
+ err['msg']
|
||||||
|
for err in ex.errors()
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
ex_name = ex.__class__.__name__
|
||||||
|
|
||||||
|
# Parse Filename and Line#
|
||||||
|
if show_filename_lineno:
|
||||||
|
_, exc_obj, exc_tb = sys.exc_info()
|
||||||
|
filename = (
|
||||||
|
Path(exc_tb.tb_frame.f_code.co_filename).name
|
||||||
|
if exc_tb is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
lineno = exc_tb.tb_lineno if exc_tb is not None else None
|
||||||
|
info = f' ({filename}|{lineno}):' if exc_tb is not None else ':'
|
||||||
|
else:
|
||||||
|
info = ':'
|
||||||
|
|
||||||
|
# Format Message Tuple
|
||||||
|
messages = [
|
||||||
|
rich.padding.Padding(rich.markdown.Markdown(arg), pad=(0, 0, 0, 4))
|
||||||
|
for arg in ex.args
|
||||||
|
]
|
||||||
|
|
||||||
|
# Present
|
||||||
|
rich.print(
|
||||||
|
f'[bold red]{ex_name}[/bold red]',
|
||||||
|
info,
|
||||||
|
*messages,
|
||||||
|
sep='',
|
||||||
|
)
|
|
@ -66,21 +66,21 @@ def find_proj_spec(proj_path: Path | None) -> Path:
|
||||||
proj_path: Search for a `pyproject.toml` project specification at this path.
|
proj_path: Search for a `pyproject.toml` project specification at this path.
|
||||||
When `None`, search in the current working directory.
|
When `None`, search in the current working directory.
|
||||||
"""
|
"""
|
||||||
|
# Deduce Project Path
|
||||||
if proj_path is None:
|
if proj_path is None:
|
||||||
path_proj_spec = Path.cwd() / 'pyproject.toml'
|
path_proj_spec = Path.cwd() / 'pyproject.toml'
|
||||||
if not path_proj_spec.is_file():
|
elif proj_path.exists():
|
||||||
symptom = (
|
if proj_path.is_file():
|
||||||
'does not exist' if not path_proj_spec.exists() else 'is not a file'
|
path_proj_spec = proj_path
|
||||||
)
|
elif proj_path.is_dir():
|
||||||
msg = "Couldn't find 'pyproject.toml' in the current working directory"
|
|
||||||
|
|
||||||
elif proj_path.is_dir() and (proj_path / 'pyproject.toml').is_file():
|
|
||||||
path_proj_spec = proj_path / 'pyproject.toml'
|
path_proj_spec = proj_path / 'pyproject.toml'
|
||||||
|
|
||||||
elif not proj_path.is_file():
|
if not path_proj_spec.is_file():
|
||||||
symptom = 'does not exist' if not proj_path.exists() else 'is not a file'
|
msgs = [
|
||||||
msg = f"Provided project specification {symptom} (tried to load '{proj_path}')"
|
f'No project specification found at `{path_proj_spec}`.',
|
||||||
raise ValueError(msg)
|
'Please verify the project path.',
|
||||||
|
]
|
||||||
|
raise ValueError(*msgs)
|
||||||
|
|
||||||
return path_proj_spec.resolve()
|
return path_proj_spec.resolve()
|
||||||
|
|
||||||
|
|
202
blext/spec.py
202
blext/spec.py
|
@ -27,6 +27,40 @@ import tomli_w
|
||||||
|
|
||||||
from . import supported
|
from . import supported
|
||||||
|
|
||||||
|
ValidBLTags: typ.TypeAlias = typ.Literal[
|
||||||
|
'3D View',
|
||||||
|
'Add Curve',
|
||||||
|
'Add Mesh',
|
||||||
|
'Animation',
|
||||||
|
'Bake',
|
||||||
|
'Camera',
|
||||||
|
'Compositing',
|
||||||
|
'Development',
|
||||||
|
'Game Engine',
|
||||||
|
'Geometry Nodes',
|
||||||
|
'Grease Pencil',
|
||||||
|
'Import-Export',
|
||||||
|
'Lighting',
|
||||||
|
'Material',
|
||||||
|
'Modeling',
|
||||||
|
'Mesh',
|
||||||
|
'Node',
|
||||||
|
'Object',
|
||||||
|
'Paint',
|
||||||
|
'Pipeline',
|
||||||
|
'Physics',
|
||||||
|
'Render',
|
||||||
|
'Rigging',
|
||||||
|
'Scene',
|
||||||
|
'Sculpt',
|
||||||
|
'Sequencer',
|
||||||
|
'System',
|
||||||
|
'Text Editor',
|
||||||
|
'Tracking',
|
||||||
|
'User Interface',
|
||||||
|
'UV',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Path Mangling
|
# - Path Mangling
|
||||||
|
@ -164,39 +198,7 @@ class BLExtSpec(pyd.BaseModel):
|
||||||
|
|
||||||
# Addon Tags
|
# Addon Tags
|
||||||
tags: tuple[
|
tags: tuple[
|
||||||
typ.Literal[
|
ValidBLTags,
|
||||||
'3D View',
|
|
||||||
'Add Curve',
|
|
||||||
'Add Mesh',
|
|
||||||
'Animation',
|
|
||||||
'Bake',
|
|
||||||
'Camera',
|
|
||||||
'Compositing',
|
|
||||||
'Development',
|
|
||||||
'Game Engine',
|
|
||||||
'Geometry Nodes',
|
|
||||||
'Grease Pencil',
|
|
||||||
'Import-Export',
|
|
||||||
'Lighting',
|
|
||||||
'Material',
|
|
||||||
'Modeling',
|
|
||||||
'Mesh',
|
|
||||||
'Node',
|
|
||||||
'Object',
|
|
||||||
'Paint',
|
|
||||||
'Pipeline',
|
|
||||||
'Physics',
|
|
||||||
'Render',
|
|
||||||
'Rigging',
|
|
||||||
'Scene',
|
|
||||||
'Sculpt',
|
|
||||||
'Sequencer',
|
|
||||||
'System',
|
|
||||||
'Text Editor',
|
|
||||||
'Tracking',
|
|
||||||
'User Interface',
|
|
||||||
'UV',
|
|
||||||
],
|
|
||||||
...,
|
...,
|
||||||
] = ()
|
] = ()
|
||||||
license: tuple[str, ...]
|
license: tuple[str, ...]
|
||||||
|
@ -389,60 +391,83 @@ class BLExtSpec(pyd.BaseModel):
|
||||||
ValueError: If the `pyproject.toml` file does not contain the required tables and/or fields.
|
ValueError: If the `pyproject.toml` file does not contain the required tables and/or fields.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Parse Sections
|
####################
|
||||||
## Parse [project]
|
# - Parsing: Stage 1
|
||||||
|
####################
|
||||||
|
###: Determine whether all fields are accessible.
|
||||||
|
|
||||||
|
# Parse [project]
|
||||||
if proj_spec.get('project') is not None:
|
if proj_spec.get('project') is not None:
|
||||||
project = proj_spec['project']
|
project = proj_spec['project']
|
||||||
else:
|
else:
|
||||||
msg = "'pyproject.toml' MUST define '[project]' table"
|
msgs = [
|
||||||
raise ValueError(msg)
|
f'In `{path_proj_root / "pyproject.toml"}`:',
|
||||||
|
'- `[project]` table is not defined.',
|
||||||
|
]
|
||||||
|
raise ValueError(*msgs)
|
||||||
|
|
||||||
## Parse [tool.blext]
|
# Parse [tool.blext]
|
||||||
if (
|
if (
|
||||||
proj_spec.get('tool') is not None
|
proj_spec.get('tool') is not None
|
||||||
or proj_spec['tool'].get('blext') is not None
|
and proj_spec['tool'].get('blext') is not None
|
||||||
):
|
):
|
||||||
blext_spec = proj_spec['tool']['blext']
|
blext_spec = proj_spec['tool']['blext']
|
||||||
else:
|
else:
|
||||||
msg = "'pyproject.toml' MUST define '[tool.blext]' table"
|
msgs = [
|
||||||
raise ValueError(msg)
|
f'In `{path_proj_root / "pyproject.toml"}`:',
|
||||||
|
'- `[tool.blext]` table is not defined.',
|
||||||
|
]
|
||||||
|
raise ValueError(*msgs)
|
||||||
|
|
||||||
## Parse [tool.blext.profiles]
|
# Parse [tool.blext.profiles]
|
||||||
if proj_spec['tool']['blext'].get('profiles') is not None:
|
if proj_spec['tool']['blext'].get('profiles') is not None:
|
||||||
release_profiles = blext_spec['profiles']
|
release_profiles = blext_spec['profiles']
|
||||||
if release_profile in release_profiles:
|
if release_profile in release_profiles:
|
||||||
release_profile_spec = release_profiles[release_profile]
|
release_profile_spec = release_profiles[release_profile]
|
||||||
else:
|
else:
|
||||||
msg = f"To parse the profile '{release_profile}' from 'pyproject.toml', it MUST be defined as a key in '[tool.blext.profiles]'"
|
msgs = [
|
||||||
raise ValueError(msg)
|
f'In `{path_proj_root / "pyproject.toml"}`:',
|
||||||
|
f'- `[tool.blext.profiles.{release_profile}]` table is not defined, yet `{release_profile}` is requested.',
|
||||||
|
]
|
||||||
|
raise ValueError(*msgs)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
msg = "'pyproject.toml' MUST define '[tool.blext.profiles]'"
|
msgs = [
|
||||||
raise ValueError(msg)
|
f'In `{path_proj_root / "pyproject.toml"}`:',
|
||||||
|
'- `[tool.blext.profiles]` table is not defined.',
|
||||||
|
]
|
||||||
|
raise ValueError(*msgs)
|
||||||
|
|
||||||
# Parse Values
|
####################
|
||||||
## Parse project.requires-python
|
# - Parsing: Stage 2
|
||||||
|
####################
|
||||||
|
###: Parse values that require transformations.
|
||||||
|
|
||||||
|
field_parse_errs = []
|
||||||
|
|
||||||
|
# Parse project.requires-python
|
||||||
if project.get('requires-python') is not None:
|
if project.get('requires-python') is not None:
|
||||||
project_requires_python = project['requires-python'].replace('~= ', '')
|
project_requires_python = project['requires-python'].replace('~= ', '')
|
||||||
else:
|
else:
|
||||||
msg = "'pyproject.toml' MUST define 'project.requires-python'"
|
field_parse_errs.append('- `project.requires-python` is not defined.')
|
||||||
raise ValueError(msg)
|
|
||||||
|
|
||||||
## Parse project.maintainers[0]
|
# Parse project.maintainers[0]
|
||||||
if project.get('maintainers') is not None:
|
if project.get('maintainers') is not None and len(project['maintainers']) > 0:
|
||||||
first_maintainer = project.get('maintainers')[0]
|
first_maintainer = project.get('maintainers')[0]
|
||||||
else:
|
else:
|
||||||
first_maintainer = {'name': None, 'email': None}
|
first_maintainer = {'name': None, 'email': None}
|
||||||
|
|
||||||
## Parse project.license
|
# Parse project.license
|
||||||
if (
|
if (
|
||||||
project.get('license') is not None
|
project.get('license') is not None
|
||||||
and project['license'].get('text') is not None
|
and project['license'].get('text') is not None
|
||||||
):
|
):
|
||||||
_license = project['license']['text']
|
_license = project['license']['text']
|
||||||
else:
|
else:
|
||||||
msg = "'pyproject.toml' MUST define 'project.license.text'"
|
field_parse_errs.append('- `project.license.text` is not defined.')
|
||||||
raise ValueError(msg)
|
field_parse_errs.append(
|
||||||
|
'- Please note that all Blender addons MUST have a GPL-compatible license: <https://docs.blender.org/manual/en/latest/advanced/extensions/licenses.html>'
|
||||||
|
)
|
||||||
|
|
||||||
## Parse project.urls.homepage
|
## Parse project.urls.homepage
|
||||||
if (
|
if (
|
||||||
|
@ -453,13 +478,54 @@ class BLExtSpec(pyd.BaseModel):
|
||||||
else:
|
else:
|
||||||
homepage = None
|
homepage = None
|
||||||
|
|
||||||
# Conform to BLExt Specification
|
####################
|
||||||
|
# - Parsing: Stage 3
|
||||||
|
####################
|
||||||
|
###: Parse field availability and provide for descriptive errors
|
||||||
|
|
||||||
|
if blext_spec.get('platforms') is None:
|
||||||
|
field_parse_errs += ['- `[tool.blext.platforms]` is not defined.']
|
||||||
|
if project.get('name') is None:
|
||||||
|
field_parse_errs += ['- `project.name` is not defined.']
|
||||||
|
if blext_spec.get('pretty_name') is None:
|
||||||
|
field_parse_errs += ['- `tool.blext.pretty_name` is not defined.']
|
||||||
|
if project.get('version') is None:
|
||||||
|
field_parse_errs += ['- `project.version` is not defined.']
|
||||||
|
if project.get('description') is None:
|
||||||
|
field_parse_errs += ['- `project.description` is not defined.']
|
||||||
|
if blext_spec.get('blender_version_min') is None:
|
||||||
|
field_parse_errs += ['- `tool.blext.blender_version_min` is not defined.']
|
||||||
|
if blext_spec.get('blender_version_max') is None:
|
||||||
|
field_parse_errs += ['- `tool.blext.blender_version_max` is not defined.']
|
||||||
|
if blext_spec.get('bl_tags') is None:
|
||||||
|
field_parse_errs += ['- `tool.blext.bl_tags` is not defined.']
|
||||||
|
field_parse_errs += [
|
||||||
|
'- Valid `bl_tags` values are: '
|
||||||
|
+ ', '.join([f'"{el}"' for el in typ.get_args(ValidBLTags)])
|
||||||
|
]
|
||||||
|
if blext_spec.get('copyright') is None:
|
||||||
|
field_parse_errs += ['- `tool.blext.copyright` is not defined.']
|
||||||
|
field_parse_errs += [
|
||||||
|
'- Example: `copyright = ["<current_year> <proj_name> Contributors`'
|
||||||
|
]
|
||||||
|
|
||||||
|
if field_parse_errs:
|
||||||
|
msgs = [
|
||||||
|
f'In `{path_proj_root / "pyproject.toml"}`:',
|
||||||
|
*field_parse_errs,
|
||||||
|
]
|
||||||
|
raise ValueError(*msgs)
|
||||||
|
|
||||||
|
####################
|
||||||
|
# - Parsing: Stage 4
|
||||||
|
####################
|
||||||
|
###: With guaranteed existance, do qualitative parsing w/pydantic.
|
||||||
return cls(
|
return cls(
|
||||||
path_proj_root=path_proj_root,
|
path_proj_root=path_proj_root,
|
||||||
req_python_version=project_requires_python,
|
req_python_version=project_requires_python,
|
||||||
bl_platform_pypa_tags=blext_spec.get('platforms'),
|
bl_platform_pypa_tags=blext_spec['platforms'],
|
||||||
# Path Locality
|
# Path Locality
|
||||||
use_path_local=release_profile_spec.get('use_path_local'),
|
use_path_local=release_profile_spec.get('use_path_local', False),
|
||||||
# File Logging
|
# File Logging
|
||||||
use_log_file=release_profile_spec.get('use_log_file', False),
|
use_log_file=release_profile_spec.get('use_log_file', False),
|
||||||
log_file_path=release_profile_spec.get('log_file_path', 'addon.log'),
|
log_file_path=release_profile_spec.get('log_file_path', 'addon.log'),
|
||||||
|
@ -468,20 +534,20 @@ class BLExtSpec(pyd.BaseModel):
|
||||||
use_log_console=release_profile_spec.get('use_log_console', True),
|
use_log_console=release_profile_spec.get('use_log_console', True),
|
||||||
log_console_level=release_profile_spec.get('log_console_level', 'DEBUG'),
|
log_console_level=release_profile_spec.get('log_console_level', 'DEBUG'),
|
||||||
# Basics
|
# Basics
|
||||||
id=project.get('name'),
|
id=project['name'],
|
||||||
name=blext_spec.get('pretty_name'),
|
name=blext_spec['pretty_name'],
|
||||||
version=project.get('version'),
|
version=project['version'],
|
||||||
tagline=project.get('description'),
|
tagline=project['description'],
|
||||||
maintainer=f'{first_maintainer["name"]} <{first_maintainer["email"]}>',
|
maintainer=f'{first_maintainer["name"]} <{first_maintainer["email"]}>',
|
||||||
# Blender Compatibility
|
# Blender Compatibility
|
||||||
blender_version_min=blext_spec.get('blender_version_min'),
|
blender_version_min=blext_spec['blender_version_min'],
|
||||||
blender_version_max=blext_spec.get('blender_version_max'),
|
blender_version_max=blext_spec['blender_version_max'],
|
||||||
# Permissions
|
# Permissions
|
||||||
permissions=blext_spec.get('permissions', {}),
|
permissions=blext_spec.get('permissions', {}),
|
||||||
# Addon Tags
|
# Addon Tags
|
||||||
tags=blext_spec.get('bl_tags'),
|
tags=blext_spec['bl_tags'],
|
||||||
license=(f'SPDX:{_license}',),
|
license=(f'SPDX:{_license}',),
|
||||||
copyright=blext_spec.get('copyright'),
|
copyright=blext_spec['copyright'],
|
||||||
website=homepage,
|
website=homepage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -499,14 +565,14 @@ class BLExtSpec(pyd.BaseModel):
|
||||||
release_profile: The profile to load initial settings for.
|
release_profile: The profile to load initial settings for.
|
||||||
|
|
||||||
Raises:
|
Raises:
|
||||||
ValueError: If the `pyproject.toml` file does not contain the required tables and/or fields.
|
ValueError: If the `pyproject.toml` file cannot be loaded, or it does not contain the required tables and/or fields.
|
||||||
"""
|
"""
|
||||||
# Load File
|
# Load File
|
||||||
if path_proj_spec.is_file():
|
if path_proj_spec.is_file():
|
||||||
with path_proj_spec.open('rb') as f:
|
with path_proj_spec.open('rb') as f:
|
||||||
proj_spec = tomllib.load(f)
|
proj_spec = tomllib.load(f)
|
||||||
else:
|
else:
|
||||||
msg = f"Could not load 'pyproject.toml' at '{path_proj_spec}"
|
msg = f'Could not load file: `{path_proj_spec}`'
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
# Parse Extension Specification
|
# Parse Extension Specification
|
||||||
|
|
|
@ -19,13 +19,14 @@
|
||||||
These settings are transported via a special TOML file that is packed into the extension zip-file when packaging for release.
|
These settings are transported via a special TOML file that is packed into the extension zip-file when packaging for release.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
|
||||||
import tomllib
|
import tomllib
|
||||||
import typing as typ
|
import typing as typ
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pydantic as pyd
|
import pydantic as pyd
|
||||||
|
|
||||||
|
from .. import __package__ as base_package
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Constants
|
# - Constants
|
||||||
####################
|
####################
|
||||||
|
@ -46,18 +47,8 @@ StrLogLevel: typ.TypeAlias = typ.Literal[
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def str_to_loglevel(obj: typ.Any) -> LogLevel | typ.Any:
|
|
||||||
if isinstance(obj, str) and obj in STR_LOG_LEVEL:
|
|
||||||
return STR_LOG_LEVEL[obj]
|
|
||||||
return obj
|
|
||||||
|
|
||||||
|
|
||||||
class InitSettings(pyd.BaseModel):
|
class InitSettings(pyd.BaseModel):
|
||||||
"""Model describing addon initialization settings, describing default settings baked into the release.
|
"""Model describing addon initialization settings, describing default settings baked into the release."""
|
||||||
|
|
||||||
Each parameter
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
use_log_file: bool
|
use_log_file: bool
|
||||||
log_file_path: Path
|
log_file_path: Path
|
||||||
|
@ -66,6 +57,20 @@ class InitSettings(pyd.BaseModel):
|
||||||
use_log_console: bool
|
use_log_console: bool
|
||||||
log_console_level: StrLogLevel
|
log_console_level: StrLogLevel
|
||||||
|
|
||||||
|
@pyd.field_validator('log_file_path', mode='after')
|
||||||
|
@classmethod
|
||||||
|
def add_bpy_to_log_file_path(cls, value: Path, _: pyd.ValidationInfo) -> Path:
|
||||||
|
import bpy
|
||||||
|
|
||||||
|
addon_dir = Path(
|
||||||
|
bpy.utils.extension_path_user(
|
||||||
|
base_package,
|
||||||
|
path='',
|
||||||
|
create=True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return addon_dir / value
|
||||||
|
|
||||||
|
|
||||||
####################
|
####################
|
||||||
# - Init Settings Management
|
# - Init Settings Management
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[project]
|
[project]
|
||||||
name = "blext"
|
name = "blext"
|
||||||
version = "0.1.0"
|
version = "0.2.0"
|
||||||
description = "A fast, convenient project manager for Blender extensions."
|
description = "A fast, convenient project manager for Blender extensions."
|
||||||
authors = [
|
authors = [
|
||||||
{ name = "Sofus Albert Høgsbro Rose", email = "blext@sofusrose.com" },
|
{ name = "Sofus Albert Høgsbro Rose", email = "blext@sofusrose.com" },
|
||||||
|
@ -49,6 +49,9 @@ blext = "blext.cli:app"
|
||||||
[tool.setuptools]
|
[tool.setuptools]
|
||||||
packages = ["blext"]
|
packages = ["blext"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["blext"]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
package = true
|
package = true
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue