feat: Better exceptions and parsing.

main
Sofus Albert Høgsbro Rose 2025-01-14 09:20:36 +01:00
parent 8ba4302b64
commit 8220491d50
Signed by: so-rose
GPG Key ID: AD901CB0F3701434
6 changed files with 343 additions and 153 deletions

View File

@ -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,25 +93,23 @@ 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()
path_proj_spec = finders.find_proj_spec(proj_path)
# Parse Blender Extension Specification
blext_spec = spec.BLExtSpec.from_proj_spec(
path_proj_spec=path_proj_spec,
release_profile=release_profile,
).constrain_to_bl_platform(bl_platform)
# Download Wheels
wheels.download_wheels(
blext_spec,
bl_platform=bl_platform, bl_platform=bl_platform,
no_prompt=False, proj_path=proj_path,
release_profile=release_profile,
) )
# Download Wheels
with exc.handle(exc.pretty, ValueError):
wheels.download_wheels(
blext_spec,
bl_platform=bl_platform,
no_prompt=False,
)
# Pack the Blender Extension # Pack the Blender Extension
pack.pack_bl_extension(blext_spec, overwrite=True) with exc.handle(exc.pretty, ValueError):
pack.pack_bl_extension(blext_spec, overwrite=True)
# Validate the Blender Extension # Validate the Blender Extension
console.print() console.print()
@ -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,16 +156,24 @@ 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
blender_exe = finders.find_blender_exe() with exc.handle(exc.pretty, ValueError):
blender_exe = finders.find_blender_exe()
# Launch Blender # Launch Blender
## - Same as running 'blender --python ./blender_scripts/bl_init.py' ## - Same as running 'blender --python ./blender_scripts/bl_init.py'
@ -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,7 +297,8 @@ 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."""
blender_exe = finders.find_blender_exe() with exc.handle(exc.pretty, ValueError):
blender_exe = finders.find_blender_exe()
rich.print(blender_exe) rich.print(blender_exe)

View File

@ -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='',
)

View File

@ -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" path_proj_spec = proj_path / 'pyproject.toml'
elif proj_path.is_dir() and (proj_path / 'pyproject.toml').is_file(): if not path_proj_spec.is_file():
path_proj_spec = proj_path / 'pyproject.toml' msgs = [
f'No project specification found at `{path_proj_spec}`.',
elif not proj_path.is_file(): 'Please verify the project path.',
symptom = 'does not exist' if not proj_path.exists() else 'is not a file' ]
msg = f"Provided project specification {symptom} (tried to load '{proj_path}')" raise ValueError(*msgs)
raise ValueError(msg)
return path_proj_spec.resolve() return path_proj_spec.resolve()

View File

@ -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

View File

@ -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

View File

@ -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