diff --git a/doc/.gitignore b/doc/.gitignore
new file mode 100644
index 0000000..ebd7b1a
--- /dev/null
+++ b/doc/.gitignore
@@ -0,0 +1,6 @@
+/.quarto/
+_site
+_sidebar.yml
+_site
+objects.json
+reference
diff --git a/doc/_quarto.yml b/doc/_quarto.yml
new file mode 100644
index 0000000..af86c41
--- /dev/null
+++ b/doc/_quarto.yml
@@ -0,0 +1,29 @@
+project:
+ type: website
+
+metadata-files:
+ - _sidebar.yml
+
+
+quartodoc:
+ # Python Package
+ source_dir: ../src
+ package: blender_maxwell
+ parser: google
+
+ # Style
+ style: pkgdown
+ title: Package Reference
+
+ # Write Sidebar Data to Dedicated Metadata File
+ sidebar: _sidebar.yml
+
+ sections:
+ - title: Blender Maxwell API
+ desc: Root package for the Blender Maxwell addon
+ contents:
+ - register
+ - unregister
+
+ - name: preferences
+ children: embedded
diff --git a/pyproject.toml b/pyproject.toml
index 0bb2082..3b3ca7f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,22 +6,23 @@ authors = [
{ name = "Sofus Albert Høgsbro Rose", email = "blender-maxwell@sofusrose.com" }
]
dependencies = [
- "tidy3d~=2.6.1",
- "pydantic~=2.6.4",
- "sympy~=1.12",
- "scipy~=1.12.0",
- "trimesh~=4.2.0",
- "networkx~=3.2.1",
- "rtree~=1.2.0",
+ "tidy3d==2.6.*",
+ "pydantic==2.6.*",
+ "sympy==1.12",
+ "scipy==1.12.*",
+ "trimesh==4.2.*",
+ "networkx==3.2.*",
+ "rich==12.5.*",
+ "rtree==1.2.*",
- # Pin Blender 4.1.0-Compatible Versions
- ## The dependency resolver will report if anything is wonky.
- "urllib3==1.26.8",
- "requests==2.27.1",
- "numpy==1.24.3",
- "idna==3.3",
- "charset-normalizer==2.0.10",
- "certifi==2021.10.8",
+ # Pin Blender 4.1.0-Compatible Versions
+ ## The dependency resolver will report if anything is wonky.
+ "urllib3==1.26.8",
+ "requests==2.27.1",
+ "numpy==1.24.3",
+ "idna==3.3",
+ "charset-normalizer==2.0.10",
+ "certifi==2021.10.8",
]
readme = "README.md"
requires-python = "~= 3.11"
@@ -39,7 +40,8 @@ dev-dependencies = [
]
[tool.rye.scripts]
-dev = "python ./scripts/run.py"
+dev = "python ./src/scripts/dev.py"
+pack = "python ./src/scripts/pack.py"
####################
@@ -47,7 +49,8 @@ dev = "python ./scripts/run.py"
####################
[tool.ruff]
target-version = "py311"
-line-length = 79
+line-length = 88
+pycodestyle.max-doc-length = 120
[tool.ruff.lint]
task-tags = ["TODO"]
@@ -62,8 +65,8 @@ select = [
"ERA", # eradicate ## Ban Commented Code
"TRY", # tryceratops ## Exception Handling Style
"B", # flake8-bugbear ## Opinionated, Probable-Bug Patterns
- #"N", # pep8-naming ## TODO: Force Good Naming Conventions
- #"D", # pydocstyle ## TODO: Force docstrings
+ "N", # pep8-naming
+ "D", # pydocstyle
"SIM", # flake8-simplify ## Sanity-Check for Code Simplification
"SLF", # flake8-self ## Ban Private Member Access
"RUF", # Ruff-specific rules ## Extra Good-To-Have Rules
@@ -100,6 +103,15 @@ ignore = [
"B008", # FastAPI uses this for Depends(), Security(), etc. .
"E701", # class foo(Parent): pass or if simple: return are perfectly elegant
"ERA001", # 'Commented-out code' seems to be just about anything to ruff
+
+ # Line Length - Controversy Incoming
+ ## Hot Take: Let the Formatter Worry about Line Length
+ ## - Yes dear reader, I'm with you. Soft wrap can go too far.
+ ## - ...but also, sometimes there are real good reasons not to split.
+ ## - Ex. I think 'one sentence per line' docstrings are a valid thing.
+ ## - Overlong lines tend to be be a code smell anyway
+ ## - We'll see if my hot takes survive the week :)
+ "E501", # Let Formatter Worry about Line Length
]
####################
diff --git a/requirements-dev.lock b/requirements-dev.lock
index 2184b93..6d23af0 100644
--- a/requirements-dev.lock
+++ b/requirements-dev.lock
@@ -57,7 +57,7 @@ matplotlib==3.8.3
# via tidy3d
mpmath==1.3.0
# via sympy
-networkx==3.2.1
+networkx==3.2
numpy==1.24.3
# via contourpy
# via h5py
@@ -77,9 +77,9 @@ partd==1.4.1
# via dask
pillow==10.2.0
# via matplotlib
-pydantic==2.6.4
+pydantic==2.6.0
# via tidy3d
-pydantic-core==2.16.3
+pydantic-core==2.16.1
# via pydantic
pygments==2.17.2
# via rich
@@ -104,7 +104,7 @@ requests==2.27.1
# via tidy3d
responses==0.23.1
# via tidy3d
-rich==12.5.1
+rich==12.5.0
# via tidy3d
rtree==1.2.0
ruff==0.3.2
@@ -117,7 +117,7 @@ shapely==2.0.3
six==1.16.0
# via python-dateutil
sympy==1.12
-tidy3d==2.6.1
+tidy3d==2.6.0
toml==0.10.2
# via tidy3d
toolz==0.12.1
diff --git a/requirements.lock b/requirements.lock
index f7ee076..9dded78 100644
--- a/requirements.lock
+++ b/requirements.lock
@@ -56,7 +56,7 @@ matplotlib==3.8.3
# via tidy3d
mpmath==1.3.0
# via sympy
-networkx==3.2.1
+networkx==3.2
numpy==1.24.3
# via contourpy
# via h5py
@@ -76,9 +76,9 @@ partd==1.4.1
# via dask
pillow==10.2.0
# via matplotlib
-pydantic==2.6.4
+pydantic==2.6.0
# via tidy3d
-pydantic-core==2.16.3
+pydantic-core==2.16.1
# via pydantic
pygments==2.17.2
# via rich
@@ -103,7 +103,7 @@ requests==2.27.1
# via tidy3d
responses==0.23.1
# via tidy3d
-rich==12.5.1
+rich==12.5.0
# via tidy3d
rtree==1.2.0
s3transfer==0.5.2
@@ -115,7 +115,7 @@ shapely==2.0.3
six==1.16.0
# via python-dateutil
sympy==1.12
-tidy3d==2.6.1
+tidy3d==2.6.0
toml==0.10.2
# via tidy3d
toolz==0.12.1
diff --git a/scripts/__init__.py b/scripts/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/src/blender_maxwell/__init__.py b/src/blender_maxwell/__init__.py
index b916652..0929e0c 100644
--- a/src/blender_maxwell/__init__.py
+++ b/src/blender_maxwell/__init__.py
@@ -1,16 +1,16 @@
-import tomllib
from pathlib import Path
-import bpy
+from . import info
+from .nodeps.utils import simple_logger
-from . import operators_nodeps, preferences, registration
-from .utils import pydeps
-from .utils import logger as _logger
+simple_logger.sync_bootstrap_logging(
+ console_level=info.BOOTSTRAP_LOG_LEVEL,
+)
-log = _logger.get()
-PATH_ADDON_ROOT = Path(__file__).resolve().parent
-with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
- PROJ_SPEC = tomllib.load(f)
+from . import nodeps, preferences, registration # noqa: E402
+from .nodeps.utils import pydeps # noqa: E402
+
+log = simple_logger.get(__name__)
####################
# - Addon Information
@@ -33,22 +33,18 @@ bl_info = {
## The mechanism is a 'dumb' - output of 'ruff fmt' MUST be basis for replacing
-def ADDON_PREFS():
- return bpy.context.preferences.addons[
- PROJ_SPEC['project']['name']
- ].preferences
-
-
####################
# - Load and Register Addon
####################
+log.info('Loading Before-Deps BL_REGISTER')
BL_REGISTER__BEFORE_DEPS = [
- *operators_nodeps.BL_REGISTER,
+ *nodeps.operators.BL_REGISTER,
*preferences.BL_REGISTER,
]
def BL_REGISTER__AFTER_DEPS(path_deps: Path):
+ log.info('Loading After-Deps BL_REGISTER')
with pydeps.importable_addon_deps(path_deps):
from . import node_trees, operators
return [
@@ -57,11 +53,18 @@ def BL_REGISTER__AFTER_DEPS(path_deps: Path):
]
-def BL_KEYMAP_ITEM_DEFS(path_deps: Path):
+log.info('Loading Before-Deps BL_KEYMAP_ITEM_DEFS')
+BL_KEYMAP_ITEM_DEFS__BEFORE_DEPS = [
+ *nodeps.operators.BL_KEYMAP_ITEM_DEFS,
+]
+
+
+def BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_deps: Path):
+ log.info('Loading After-Deps BL_KEYMAP_ITEM_DEFS')
with pydeps.importable_addon_deps(path_deps):
from . import operators
return [
- *operators.BL_KMI_REGISTER,
+ *operators.BL_KEYMAP_ITEM_DEFS,
]
@@ -69,29 +72,44 @@ def BL_KEYMAP_ITEM_DEFS(path_deps: Path):
# - Registration
####################
def register():
+ """Register the Blender addon."""
+ log.info('Starting %s Registration', info.ADDON_NAME)
+
# Register Barebones Addon for Dependency Installation
registration.register_classes(BL_REGISTER__BEFORE_DEPS)
+ registration.register_keymap_items(BL_KEYMAP_ITEM_DEFS__BEFORE_DEPS)
# Retrieve PyDeps Path from Addon Preferences
- addon_prefs = ADDON_PREFS()
- path_pydeps = addon_prefs.path_addon_pydeps
+ if (addon_prefs := info.addon_prefs()) is None:
+ unregister()
+ msg = f'Addon preferences not found; aborting registration of {info.ADDON_NAME}'
+ raise RuntimeError(msg)
+ log.debug('Found Addon Preferences')
+
+ # Retrieve PyDeps Path
+ path_pydeps = addon_prefs.pydeps_path
+ log.info('Loaded PyDeps Path from Addon Prefs: %s', path_pydeps)
- # If Dependencies are Satisfied, Register Everything
if pydeps.check_pydeps(path_pydeps):
- registration.register_classes(BL_REGISTER__AFTER_DEPS())
- registration.register_keymap_items(BL_KEYMAP_ITEM_DEFS())
+ log.info('PyDeps Satisfied: Loading Addon %s', info.ADDON_NAME)
+ registration.register_classes(BL_REGISTER__AFTER_DEPS(path_pydeps))
+ registration.register_keymap_items(BL_KEYMAP_ITEM_DEFS__AFTER_DEPS(path_pydeps))
else:
- # Delay Registration
+ log.info(
+ 'PyDeps Invalid: Delaying Addon Registration of %s',
+ info.ADDON_NAME,
+ )
registration.delay_registration(
registration.EVENT__DEPS_SATISFIED,
classes_cb=BL_REGISTER__AFTER_DEPS,
- keymap_item_defs_cb=BL_KEYMAP_ITEM_DEFS,
+ keymap_item_defs_cb=BL_KEYMAP_ITEM_DEFS__AFTER_DEPS,
)
-
- # TODO: A popup before the addon fully loads or something like that?
- ## TODO: Communicate that deps must be installed and all that?
+ ## TODO: bpy Popup to Deal w/Dependency Errors
def unregister():
+ """Unregister the Blender addon."""
+ log.info('Starting %s Unregister', info.ADDON_NAME)
registration.unregister_classes()
registration.unregister_keymap_items()
+ log.info('Finished %s Unregister', info.ADDON_NAME)
diff --git a/src/blender_maxwell/info.py b/src/blender_maxwell/info.py
new file mode 100644
index 0000000..5a28239
--- /dev/null
+++ b/src/blender_maxwell/info.py
@@ -0,0 +1,43 @@
+import tomllib
+from pathlib import Path
+
+import bpy
+
+####################
+# - Addon Info
+####################
+PATH_ADDON_ROOT = Path(__file__).resolve().parent
+
+# Addon Information
+## bl_info is filled with PROJ_SPEC when packing the .zip.
+with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
+ PROJ_SPEC = tomllib.load(f)
+
+ADDON_NAME = PROJ_SPEC['project']['name']
+ADDON_VERSION = PROJ_SPEC['project']['version']
+
+# PyDeps Path Info
+## requirements.lock is written when packing the .zip.
+## By default, the addon pydeps are kept in the addon dir.
+PATH_REQS = PATH_ADDON_ROOT / 'requirements.lock'
+DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies'
+
+# Logging Info
+## By default, the addon file log writes to the addon dir.
+## The initial .log_level contents are written when packing the .zip.
+## Subsequent changes are managed by nodeps.utils.simple_logger.py.
+DEFAULT_LOG_PATH = PATH_ADDON_ROOT / 'addon.log'
+DEFAULT_LOG_PATH.touch(exist_ok=True)
+
+PATH_BOOTSTRAP_LOG_LEVEL = PATH_ADDON_ROOT / '.bootstrap_log_level'
+with PATH_BOOTSTRAP_LOG_LEVEL.open('r') as f:
+ BOOTSTRAP_LOG_LEVEL = int(f.read().strip())
+
+####################
+# - Addon Getters
+####################
+def addon_prefs() -> bpy.types.AddonPreferences | None:
+ if (addon := bpy.context.preferences.addons.get(ADDON_NAME)) is None:
+ return None
+
+ return addon.preferences
diff --git a/src/blender_maxwell/node_trees/__init__.py b/src/blender_maxwell/node_trees/__init__.py
index 80f25bd..515c59e 100644
--- a/src/blender_maxwell/node_trees/__init__.py
+++ b/src/blender_maxwell/node_trees/__init__.py
@@ -3,7 +3,3 @@ from . import maxwell_sim_nodes
BL_REGISTER = [
*maxwell_sim_nodes.BL_REGISTER,
]
-
-BL_NODE_CATEGORIES = [
- *maxwell_sim_nodes.BL_NODE_CATEGORIES,
-]
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py
index 3f0adff..86e68f7 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/__init__.py
@@ -16,7 +16,3 @@ BL_REGISTER = [
*nodes.BL_REGISTER,
*categories.BL_REGISTER,
]
-
-BL_NODE_CATEGORIES = [
- *categories.BL_NODE_CATEGORIES,
-]
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py
index 0859ce6..e350e46 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/bl_socket_map.py
@@ -1,16 +1,16 @@
import typing as typ
-import typing_extensions as typx
-
-import pydantic as pyd
-import sympy as sp
-import sympy.physics.units as spu
import bpy
+import sympy as sp
+import sympy.physics.units as spu
+import typing_extensions as typx
-from ...utils import extra_sympy_units as spuex
+from ...utils import logger as _logger
from . import contracts as ct
-from .contracts import SocketType as ST
from . import sockets as sck
+from .contracts import SocketType as ST
+
+log = _logger.get(__name__)
# TODO: Caching?
# TODO: Move the manual labor stuff to contracts
@@ -38,7 +38,7 @@ for socket_type in ST:
sck,
socket_type.value.removesuffix('SocketType') + 'SocketDef',
):
- print('Missing SocketDef for', socket_type.value)
+ log.warning('Missing SocketDef for %s', socket_type.value)
####################
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py
index ef15d25..8383c60 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/__init__.py
@@ -1,3 +1,5 @@
+# ruff: noqa: I001
+
####################
# - String Types
####################
@@ -5,6 +7,7 @@ from .bl import SocketName
from .bl import PresetName
from .bl import ManagedObjName
+
from .bl import BLEnumID
from .bl import BLColorRGBA
@@ -54,3 +57,29 @@ from .data_flows import DataFlowKind
# - Schemas
####################
from . import schemas
+
+####################
+# - Export
+####################
+__all__ = [
+ 'SocketName',
+ 'PresetName',
+ 'ManagedObjName',
+ 'BLEnumID',
+ 'BLColorRGBA',
+ 'Icon',
+ 'TreeType',
+ 'SocketType',
+ 'SOCKET_UNITS',
+ 'SOCKET_COLORS',
+ 'SOCKET_SHAPES',
+ 'BL_SOCKET_DESCR_TYPE_MAP',
+ 'BL_SOCKET_DIRECT_TYPE_MAP',
+ 'BL_SOCKET_DESCR_ANNOT_STRING',
+ 'NodeType',
+ 'NodeCategory',
+ 'NODE_CAT_LABELS',
+ 'ManagedObjType',
+ 'DataFlowKind',
+ 'schemas',
+]
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py
index 3bc0218..66a5cae 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/bl.py
@@ -1,9 +1,6 @@
-import typing as typ
import pydantic as pyd
import typing_extensions as pytypes_ext
-import bpy
-
####################
# - Pure BL Types
####################
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/__init__.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/__init__.py
index 62c5f4f..ae4047e 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/__init__.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/__init__.py
@@ -1,4 +1,11 @@
-from .preset_def import PresetDef
-from .socket_def import SocketDef
from .managed_obj import ManagedObj
from .managed_obj_def import ManagedObjDef
+from .preset_def import PresetDef
+from .socket_def import SocketDef
+
+__all__ = [
+ 'SocketDef',
+ 'ManagedObj',
+ 'ManagedObjDef',
+ 'PresetDef',
+]
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj.py
index e0a3b77..a589239 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj.py
@@ -19,8 +19,4 @@ class ManagedObj(typ.Protocol):
def free(self): ...
- def bl_select(self):
- """If this is a managed Blender object, and the operation "select this in Blender" makes sense, then do so.
-
- Else, do nothing.
- """
+ def bl_select(self): ...
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj_def.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj_def.py
index 9e80804..3405a81 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj_def.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/managed_obj_def.py
@@ -1,9 +1,7 @@
import typing as typ
-from dataclasses import dataclass
import pydantic as pyd
-from ..bl import PresetName, SocketName, BLEnumID
from .managed_obj import ManagedObj
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/preset_def.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/preset_def.py
index 7089809..4ef1a2b 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/preset_def.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/contracts/schemas/preset_def.py
@@ -2,7 +2,7 @@ import typing as typ
import pydantic as pyd
-from ..bl import PresetName, SocketName, BLEnumID
+from ..bl import PresetName, SocketName
class PresetDef(pyd.BaseModel):
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py
index 66727ca..4eaa08d 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/base.py
@@ -7,9 +7,12 @@ import bpy
import pydantic as pyd
import typing_extensions as typx
+from ....utils import logger
from .. import contracts as ct
from .. import sockets
+log = logger.get(__name__)
+
CACHE: dict[str, typ.Any] = {} ## By Instance UUID
## NOTE: CACHE does not persist between file loads.
@@ -56,6 +59,7 @@ class MaxwellSimNode(bpy.types.Node):
####################
def __init_subclass__(cls, **kwargs: typ.Any):
super().__init_subclass__(**kwargs)
+ log.debug('Initializing Node: %s', cls.node_type)
# Setup Blender ID for Node
if not hasattr(cls, 'node_type'):
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/web_importers/tidy_3d_web_importer.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/web_importers/tidy_3d_web_importer.py
index 62cfbee..8216da8 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/web_importers/tidy_3d_web_importer.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/inputs/web_importers/tidy_3d_web_importer.py
@@ -1,16 +1,10 @@
-import functools
import tempfile
from pathlib import Path
-import typing as typ
-from pathlib import Path
-import bpy
-import sympy as sp
-import pydantic as pyd
import tidy3d as td
import tidy3d.web as td_web
-from ......utils import tdcloud
+from ......services import tdcloud
from .... import contracts as ct
from .... import sockets
from ... import base
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py
index ad70f59..7dc32cd 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/nodes/outputs/exporters/tidy3d_web_exporter.py
@@ -1,17 +1,6 @@
-import json
-import tempfile
-import functools
-import typing as typ
-import json
-from pathlib import Path
-
import bpy
-import sympy as sp
-import pydantic as pyd
-import tidy3d as td
-import tidy3d.web as _td_web
-from ......utils import tdcloud
+from ......services import tdcloud
from .... import contracts as ct
from .... import sockets
from ... import base
diff --git a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py
index f3388ef..24a94c1 100644
--- a/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py
+++ b/src/blender_maxwell/node_trees/maxwell_sim_nodes/sockets/tidy3d/cloud_task.py
@@ -1,14 +1,9 @@
-import typing as typ
-import tempfile
-
import bpy
import pydantic as pyd
-import tidy3d as td
-import tidy3d.web as _td_web
-from .....utils import tdcloud
-from .. import base
+from .....services import tdcloud
from ... import contracts as ct
+from .. import base
####################
@@ -21,7 +16,6 @@ class ReloadFolderList(bpy.types.Operator):
@classmethod
def poll(cls, context):
- space = context.space_data
return (
tdcloud.IS_AUTHENTICATED
and hasattr(context, 'socket')
@@ -94,7 +88,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
existing_folder_id: bpy.props.EnumProperty(
name='Folder of Cloud Tasks',
description='An existing folder on the Tidy3D Cloud',
- items=lambda self, context: self.retrieve_folders(context),
+ items=lambda self, _: self.retrieve_folders(),
update=(
lambda self, context: self.sync_prop('existing_folder_id', context)
),
@@ -102,7 +96,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
existing_task_id: bpy.props.EnumProperty(
name='Existing Cloud Task',
description='An existing task on the Tidy3D Cloud, within the given folder',
- items=lambda self, context: self.retrieve_tasks(context),
+ items=lambda self, _: self.retrieve_tasks(),
update=(
lambda self, context: self.sync_prop('existing_task_id', context)
),
@@ -122,14 +116,14 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
# - Property Methods
####################
def sync_existing_folder_id(self, context):
- folder_task_ids = self.retrieve_tasks(context)
+ folder_task_ids = self.retrieve_tasks()
self.existing_task_id = folder_task_ids[0][0]
## There's guaranteed to at least be one element, even if it's "NONE".
self.sync_prop('existing_folder_id', context)
- def retrieve_folders(self, context) -> list[tuple]:
+ def retrieve_folders(self) -> list[tuple]:
folders = tdcloud.TidyCloudFolders.folders()
if not folders:
return [('NONE', 'None', 'No folders')]
@@ -143,7 +137,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
for folder_id, cloud_folder in folders.items()
]
- def retrieve_tasks(self, context) -> list[tuple]:
+ def retrieve_tasks(self) -> list[tuple]:
if (
cloud_folder := tdcloud.TidyCloudFolders.folders().get(
self.existing_folder_id
@@ -212,7 +206,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
# Propagate along Link
if self.is_linked:
msg = (
- f'Cannot sync newly created task to linked Cloud Task socket.'
+ 'Cannot sync newly created task to linked Cloud Task socket.'
)
raise ValueError(msg)
## TODO: A little aggressive. Is there a good use case?
@@ -230,7 +224,7 @@ class Tidy3DCloudTaskBLSocket(base.MaxwellSimSocket):
# Propagate along Link
if self.is_linked:
msg = (
- f'Cannot sync newly created task to linked Cloud Task socket.'
+ 'Cannot sync newly created task to linked Cloud Task socket.'
)
raise ValueError(msg)
## TODO: A little aggressive. Is there a good use case?
diff --git a/src/blender_maxwell/nodeps/__init__.py b/src/blender_maxwell/nodeps/__init__.py
new file mode 100644
index 0000000..3fb7831
--- /dev/null
+++ b/src/blender_maxwell/nodeps/__init__.py
@@ -0,0 +1,3 @@
+from . import operators, utils
+
+__all__ = ['operators', 'utils']
diff --git a/src/blender_maxwell/nodeps/operators/__init__.py b/src/blender_maxwell/nodeps/operators/__init__.py
new file mode 100644
index 0000000..f5c245e
--- /dev/null
+++ b/src/blender_maxwell/nodeps/operators/__init__.py
@@ -0,0 +1,14 @@
+from . import install_deps, uninstall_deps
+
+BL_REGISTER = [
+ *install_deps.BL_REGISTER,
+ *uninstall_deps.BL_REGISTER,
+]
+
+BL_KEYMAP_ITEM_DEFS = [
+ *install_deps.BL_KEYMAP_ITEM_DEFS,
+ *uninstall_deps.BL_KEYMAP_ITEM_DEFS,
+]
+
+
+__all__ = []
diff --git a/src/blender_maxwell/operators_nodeps/install_deps.py b/src/blender_maxwell/nodeps/operators/install_deps.py
similarity index 55%
rename from src/blender_maxwell/operators_nodeps/install_deps.py
rename to src/blender_maxwell/nodeps/operators/install_deps.py
index 6521f9d..ff339ab 100644
--- a/src/blender_maxwell/operators_nodeps/install_deps.py
+++ b/src/blender_maxwell/nodeps/operators/install_deps.py
@@ -4,7 +4,10 @@ from pathlib import Path
import bpy
-from .. import registration
+from ... import registration
+from ..utils import pydeps, simple_logger
+
+log = simple_logger.get(__name__)
class InstallPyDeps(bpy.types.Operator):
@@ -12,15 +15,30 @@ class InstallPyDeps(bpy.types.Operator):
bl_label = 'Install BLMaxwell Python Deps'
path_addon_pydeps: bpy.props.StringProperty(
- name='Path to Addon Python Dependencies'
+ name='Path to Addon Python Dependencies',
+ default='',
)
path_addon_reqs: bpy.props.StringProperty(
- name='Path to Addon Python Dependencies'
+ name='Path to Addon Python Dependencies',
+ default='',
)
+ @classmethod
+ def poll(cls, _: bpy.types.Context):
+ return not pydeps.DEPS_OK
+
def execute(self, _: bpy.types.Context):
+ if self.path_addon_pydeps == '' or self.path_addon_reqs == '':
+ msg = f"A path for operator {self.bl_idname} isn't set"
+ raise ValueError(msg)
+
path_addon_pydeps = Path(self.path_addon_pydeps)
path_addon_reqs = Path(self.path_addon_reqs)
+ log.info(
+ 'Running Install PyDeps w/requirements.txt (%s) to path: %s',
+ path_addon_reqs,
+ path_addon_pydeps,
+ )
# Create the Addon-Specific Folder (if Needed)
## It MUST, however, have a parent already
@@ -34,21 +52,23 @@ class InstallPyDeps(bpy.types.Operator):
# Install Deps w/Bundled pip
try:
- subprocess.check_call(
- [
- str(python_exec),
- '-m',
- 'pip',
- 'install',
- '-r',
- str(path_addon_reqs),
- '--target',
- str(path_addon_pydeps),
- ]
+ cmdline = [
+ str(python_exec),
+ '-m',
+ 'pip',
+ 'install',
+ '-r',
+ str(path_addon_reqs),
+ '--target',
+ str(path_addon_pydeps),
+ ]
+ log.info(
+ 'Running pip w/cmdline: %s',
+ ' '.join(cmdline),
)
- except subprocess.CalledProcessError as e:
- msg = f'Failed to install dependencies: {str(e)}'
- self.report({'ERROR'}, msg)
+ subprocess.check_call(cmdline)
+ except subprocess.CalledProcessError:
+ log.exception('Failed to install PyDeps')
return {'CANCELLED'}
registration.run_delayed_registration(
@@ -64,3 +84,4 @@ class InstallPyDeps(bpy.types.Operator):
BL_REGISTER = [
InstallPyDeps,
]
+BL_KEYMAP_ITEM_DEFS = []
diff --git a/src/blender_maxwell/nodeps/operators/popup_install_deps.py b/src/blender_maxwell/nodeps/operators/popup_install_deps.py
new file mode 100644
index 0000000..ff4420b
--- /dev/null
+++ b/src/blender_maxwell/nodeps/operators/popup_install_deps.py
@@ -0,0 +1,85 @@
+import subprocess
+import sys
+from pathlib import Path
+
+import bpy
+
+from .. import registration
+from ..utils import logger as _logger
+
+log = _logger.get(__name__)
+
+
+class InstallPyDeps(bpy.types.Operator):
+ bl_idname = 'blender_maxwell.nodeps__addon_install_popup'
+ bl_label = 'Popup to Install BLMaxwell Python Deps'
+
+ path_addon_pydeps: bpy.props.StringProperty(
+ name='Path to Addon Python Dependencies',
+ default='',
+ )
+ path_addon_reqs: bpy.props.StringProperty(
+ name='Path to Addon Python Dependencies',
+ default='',
+ )
+
+ # TODO: poll()
+
+ def execute(self, _: bpy.types.Context):
+ if self.path_addon_pydeps == '' or self.path_addon_reqs == '':
+ msg = f"A path for operator {self.bl_idname} isn't set"
+ raise ValueError(msg)
+
+ path_addon_pydeps = Path(self.path_addon_pydeps)
+ path_addon_reqs = Path(self.path_addon_reqs)
+ log.info(
+ 'Running Install PyDeps w/requirements.txt (%s) to path: %s',
+ path_addon_reqs,
+ path_addon_pydeps,
+ )
+
+ # Create the Addon-Specific Folder (if Needed)
+ ## It MUST, however, have a parent already
+ path_addon_pydeps.mkdir(parents=False, exist_ok=True)
+
+ # Determine Path to Blender's Bundled Python
+ ## bpy.app.binary_path_python was deprecated in 2.91.
+ ## sys.executable points to the correct bundled Python.
+ ## See
+ python_exec = Path(sys.executable)
+
+ # Install Deps w/Bundled pip
+ try:
+ cmdline = [
+ str(python_exec),
+ '-m',
+ 'pip',
+ 'install',
+ '-r',
+ str(path_addon_reqs),
+ '--target',
+ str(path_addon_pydeps),
+ ]
+ log.info(
+ 'Running pip w/cmdline: %s',
+ ' '.join(cmdline),
+ )
+ subprocess.check_call(cmdline)
+ except subprocess.CalledProcessError:
+ log.exception('Failed to install PyDeps')
+ return {'CANCELLED'}
+
+ registration.run_delayed_registration(
+ registration.EVENT__ON_DEPS_INSTALLED,
+ path_addon_pydeps,
+ )
+ return {'FINISHED'}
+
+
+####################
+# - Blender Registration
+####################
+BL_REGISTER = [
+ InstallPyDeps,
+]
+BL_KEYMAP_ITEM_DEFS = []
diff --git a/src/blender_maxwell/operators_nodeps/uninstall_deps.py b/src/blender_maxwell/nodeps/operators/uninstall_deps.py
similarity index 79%
rename from src/blender_maxwell/operators_nodeps/uninstall_deps.py
rename to src/blender_maxwell/nodeps/operators/uninstall_deps.py
index 3d98b4e..ace96bb 100644
--- a/src/blender_maxwell/operators_nodeps/uninstall_deps.py
+++ b/src/blender_maxwell/nodeps/operators/uninstall_deps.py
@@ -1,9 +1,9 @@
import shutil
+from pathlib import Path
import bpy
from ..utils import pydeps
-from .. import registration
class UninstallPyDeps(bpy.types.Operator):
@@ -14,7 +14,12 @@ class UninstallPyDeps(bpy.types.Operator):
name='Path to Addon Python Dependencies'
)
+ @classmethod
+ def poll(cls, _: bpy.types.Context):
+ return pydeps.DEPS_OK
+
def execute(self, _: bpy.types.Context):
+ path_addon_pydeps = Path(self.path_addon_pydeps)
if (
pydeps.check_pydeps()
and self.path_addon_pydeps.exists()
@@ -35,3 +40,4 @@ class UninstallPyDeps(bpy.types.Operator):
BL_REGISTER = [
UninstallPyDeps,
]
+BL_KEYMAP_ITEM_DEFS = []
diff --git a/src/blender_maxwell/nodeps/utils/__init__.py b/src/blender_maxwell/nodeps/utils/__init__.py
new file mode 100644
index 0000000..8cba849
--- /dev/null
+++ b/src/blender_maxwell/nodeps/utils/__init__.py
@@ -0,0 +1,3 @@
+from . import pydeps
+
+__all__ = ['pydeps']
diff --git a/src/blender_maxwell/utils/pydeps.py b/src/blender_maxwell/nodeps/utils/pydeps.py
similarity index 70%
rename from src/blender_maxwell/utils/pydeps.py
rename to src/blender_maxwell/nodeps/utils/pydeps.py
index e651d4b..600d1f5 100644
--- a/src/blender_maxwell/utils/pydeps.py
+++ b/src/blender_maxwell/nodeps/utils/pydeps.py
@@ -4,17 +4,10 @@ import os
import sys
from pathlib import Path
-from . import logger as _logger
+from ... import info
+from . import simple_logger
-log = _logger.get()
-
-####################
-# - Constants
-####################
-PATH_ADDON_ROOT = Path(__file__).resolve().parent.parent
-PATH_REQS = PATH_ADDON_ROOT / 'requirements.txt'
-DEFAULT_PATH_DEPS = PATH_ADDON_ROOT / '.addon_dependencies'
-DEFAULT_PATH_DEPS.mkdir(exist_ok=True)
+log = simple_logger.get(__name__)
####################
# - Globals
@@ -29,21 +22,39 @@ DEPS_ISSUES: list[str] | None = None
@contextlib.contextmanager
def importable_addon_deps(path_deps: Path):
os_path = os.fspath(path_deps)
+
+ log.info('Adding Path to sys.path: %s', str(os_path))
sys.path.insert(0, os_path)
try:
yield
finally:
+ log.info('Removing Path from sys.path: %s', str(os_path))
sys.path.remove(os_path)
+@contextlib.contextmanager
+def syspath_from_bpy_prefs() -> bool:
+ import bpy
+
+ addon_prefs = bpy.context.preferences.addons[info.ADDON_NAME].preferences
+ if hasattr(addon_prefs, 'path_addon_pydeps'):
+ log.info('Retrieved PyDeps Path from Addon Prefs')
+ path_pydeps = addon_prefs.path_addon_pydeps
+ with importable_addon_deps(path_pydeps):
+ yield True
+ else:
+ log.info("Couldn't PyDeps Path from Addon Prefs")
+ yield False
+
+
####################
# - Check PyDeps
####################
def _check_pydeps(
- path_requirementstxt: Path,
+ path_requirementslock: Path,
path_deps: Path,
) -> dict[str, tuple[str, str]]:
- """Check if packages defined in a 'requirements.txt' file are currently installed.
+ """Check if packages defined in a 'requirements.lock' file are currently installed.
Returns a list of any issues (if empty, then all dependencies are correctly satisfied).
"""
@@ -54,7 +65,7 @@ def _check_pydeps(
See """
return deplock.lower().replace('_', '-')
- with path_requirementstxt.open('r') as file:
+ with path_requirementslock.open('r') as file:
required_depslock = {
conform_pypi_package_deplock(line)
for raw_line in file.readlines()
@@ -108,14 +119,15 @@ def check_pydeps(path_deps: Path):
global DEPS_OK # noqa: PLW0603
global DEPS_ISSUES # noqa: PLW0603
- if len(_issues := _check_pydeps(PATH_REQS, path_deps)) > 0:
- # log.debug('Package Check Failed:', end='\n\t')
- # log.debug(*_issues, sep='\n\t')
+ if len(issues := _check_pydeps(info.PATH_REQS, path_deps)) > 0:
+ log.info('PyDeps Check Failed')
+ log.debug('%s', ', '.join(issues))
DEPS_OK = False
- DEPS_ISSUES = _issues
+ DEPS_ISSUES = issues
else:
+ log.info('PyDeps Check Succeeded')
DEPS_OK = True
- DEPS_ISSUES = _issues
+ DEPS_ISSUES = []
return DEPS_OK
diff --git a/src/blender_maxwell/nodeps/utils/simple_logger.py b/src/blender_maxwell/nodeps/utils/simple_logger.py
new file mode 100644
index 0000000..8922b36
--- /dev/null
+++ b/src/blender_maxwell/nodeps/utils/simple_logger.py
@@ -0,0 +1,166 @@
+import logging
+import typing as typ
+from pathlib import Path
+
+LogLevel: typ.TypeAlias = int
+LogHandler: typ.TypeAlias = typ.Any ## TODO: Can we do better?
+
+####################
+# - Constants
+####################
+LOG_LEVEL_MAP: dict[str, LogLevel] = {
+ 'DEBUG': logging.DEBUG,
+ 'INFO': logging.INFO,
+ 'WARNING': logging.WARNING,
+ 'ERROR': logging.ERROR,
+ 'CRITICAL': logging.CRITICAL,
+}
+
+SIMPLE_LOGGER_PREFIX = 'simple::'
+
+STREAM_LOG_FORMAT = 11*' ' + '%(levelname)-8s %(message)s (%(name)s)'
+FILE_LOG_FORMAT = STREAM_LOG_FORMAT
+
+####################
+# - Globals
+####################
+CACHE = {
+ 'console_level': None,
+ 'file_path': None,
+ 'file_level': logging.NOTSET,
+}
+
+
+####################
+# - Logging Handlers
+####################
+def console_handler(level: LogLevel) -> logging.StreamHandler:
+ stream_formatter = logging.Formatter(STREAM_LOG_FORMAT)
+ stream_handler = logging.StreamHandler()
+ stream_handler.setFormatter(stream_formatter)
+ stream_handler.setLevel(level)
+ return stream_handler
+
+
+def file_handler(path_log_file: Path, level: LogLevel) -> logging.FileHandler:
+ file_formatter = logging.Formatter(FILE_LOG_FORMAT)
+ file_handler = logging.FileHandler(path_log_file)
+ file_handler.setFormatter(file_formatter)
+ file_handler.setLevel(level)
+ return file_handler
+
+
+####################
+# - Logger Setup
+####################
+def setup_logger(
+ cb_console_handler: typ.Callable[[LogLevel], LogHandler],
+ cb_file_handler: typ.Callable[[Path, LogLevel], LogHandler],
+ logger: logging.Logger,
+ console_level: LogLevel | None,
+ file_path: Path | None,
+ file_level: LogLevel,
+):
+ # Delegate Level Semantics to Log Handlers
+ ## This lets everything through
+ logger.setLevel(logging.DEBUG)
+
+ # DO NOT Propagate to Root Logger
+ ## This looks like 'double messages'
+ logger.propagate = False
+ ## See SO/6729268/log-messages-appearing-twice-with-python-logging
+
+ # Clear Existing Handlers
+ if logger.handlers:
+ logger.handlers.clear()
+
+ # Add Console Logging Handler
+ if console_level is not None:
+ logger.addHandler(cb_console_handler(console_level))
+
+ # Add File Logging Handler
+ if file_path is not None:
+ logger.addHandler(cb_file_handler(file_path, file_level))
+
+
+def get(module_name):
+ logger = logging.getLogger(SIMPLE_LOGGER_PREFIX + module_name)
+
+ # Reuse Cached Arguments from Last sync_*
+ setup_logger(
+ console_handler,
+ file_handler,
+ logger,
+ console_level=CACHE['console_level'],
+ file_path=CACHE['file_path'],
+ file_level=CACHE['file_level'],
+ )
+
+ return logger
+
+
+####################
+# - Logger Sync
+####################
+def sync_bootstrap_logging(
+ console_level: LogLevel | None = None,
+ file_path: Path | None = None,
+ file_level: LogLevel = logging.NOTSET,
+):
+ CACHE['console_level'] = console_level
+ CACHE['file_path'] = file_path
+ CACHE['file_level'] = file_level
+
+ logger_logger = logging.getLogger(__name__)
+ for name in logging.root.manager.loggerDict:
+ logger = logging.getLogger(name)
+ setup_logger(
+ console_handler,
+ file_handler,
+ logger,
+ console_level=console_level,
+ file_path=file_path,
+ file_level=file_level,
+ )
+ logger_logger.info("Bootstrapped Logging w/Settings %s", str(CACHE))
+
+
+def sync_loggers(
+ cb_console_handler: typ.Callable[[LogLevel], LogHandler],
+ cb_file_handler: typ.Callable[[Path, LogLevel], LogHandler],
+ console_level: LogLevel | None,
+ file_path: Path | None,
+ file_level: LogLevel,
+):
+ """Update all loggers to conform to the given per-handler on/off state and log level."""
+ CACHE['console_level'] = console_level
+ CACHE['file_path'] = file_path
+ CACHE['file_level'] = file_level
+
+ for name in logging.root.manager.loggerDict:
+ logger = logging.getLogger(name)
+ setup_logger(
+ cb_console_handler,
+ cb_file_handler,
+ logger,
+ console_level=console_level,
+ file_path=file_path,
+ file_level=file_level,
+ )
+
+
+####################
+# - Logger Iteration
+####################
+def loggers():
+ return [
+ logging.getLogger(name) for name in logging.root.manager.loggerDict
+ ]
+
+
+def simple_loggers():
+ return [
+ logging.getLogger(name)
+ for name in logging.root.manager.loggerDict
+ if name.startswith(SIMPLE_LOGGER_PREFIX)
+ ]
diff --git a/src/blender_maxwell/operators/__init__.py b/src/blender_maxwell/operators/__init__.py
index ee6095f..32c10ed 100644
--- a/src/blender_maxwell/operators/__init__.py
+++ b/src/blender_maxwell/operators/__init__.py
@@ -3,6 +3,6 @@ from . import connect_viewer
BL_REGISTER = [
*connect_viewer.BL_REGISTER,
]
-BL_KMI_REGISTER = [
- *connect_viewer.BL_KMI_REGISTER,
+BL_KEYMAP_ITEM_DEFS = [
+ *connect_viewer.BL_KEYMAP_ITEM_DEFS,
]
diff --git a/src/blender_maxwell/operators/bl_append.py b/src/blender_maxwell/operators/bl_append.py
index af6d2ed..e69de29 100644
--- a/src/blender_maxwell/operators/bl_append.py
+++ b/src/blender_maxwell/operators/bl_append.py
@@ -1,33 +0,0 @@
-import sys
-import shutil
-import subprocess
-from pathlib import Path
-
-import bpy
-
-from . import types
-
-
-class BlenderMaxwellUninstallDependenciesOperator(bpy.types.Operator):
- bl_idname = types.BlenderMaxwellUninstallDependencies
- bl_label = 'Uninstall Dependencies for Blender Maxwell Addon'
-
- def execute(self, context):
- addon_dir = Path(__file__).parent.parent
- # addon_specific_folder = addon_dir / '.dependencies'
- addon_specific_folder = Path(
- '/home/sofus/src/college/bsc_ge/thesis/code/.cached-dependencies'
- )
-
- shutil.rmtree(addon_specific_folder)
-
- return {'FINISHED'}
-
-
-####################
-# - Blender Registration
-####################
-BL_REGISTER = [
- BlenderMaxwellUninstallDependenciesOperator,
-]
-BL_KMI_REGISTER = []
diff --git a/src/blender_maxwell/operators/connect_viewer.py b/src/blender_maxwell/operators/connect_viewer.py
index fb94ecd..3f172b9 100644
--- a/src/blender_maxwell/operators/connect_viewer.py
+++ b/src/blender_maxwell/operators/connect_viewer.py
@@ -1,5 +1,9 @@
import bpy
+from ..utils import logger as logger
+
+log = logger.get(__name__)
+
class ConnectViewerNode(bpy.types.Operator):
bl_idname = 'blender_maxwell.connect_viewer_node'
@@ -55,7 +59,7 @@ BL_REGISTER = [
ConnectViewerNode,
]
-BL_KMI_REGISTER = [
+BL_KEYMAP_ITEM_DEFS = [
{
'_': (
ConnectViewerNode.bl_idname,
diff --git a/src/blender_maxwell/operators_nodeps/__init__.py b/src/blender_maxwell/operators_nodeps/__init__.py
deleted file mode 100644
index 76a0da7..0000000
--- a/src/blender_maxwell/operators_nodeps/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-from . import install_deps
-from . import uninstall_deps
-
-BL_REGISTER = [
- *install_deps.BL_REGISTER,
- *uninstall_deps.BL_REGISTER,
-]
diff --git a/src/blender_maxwell/preferences.py b/src/blender_maxwell/preferences.py
index f6236b4..e111db9 100644
--- a/src/blender_maxwell/preferences.py
+++ b/src/blender_maxwell/preferences.py
@@ -1,149 +1,283 @@
-import tomllib
+import logging
from pathlib import Path
import bpy
-from . import registration
-from .operators_nodeps import install_deps, uninstall_deps
-from .utils import logger as _logger
-from .utils import pydeps
+from . import info, registration
+from .nodeps.operators import install_deps, uninstall_deps
+from .nodeps.utils import pydeps, simple_logger
####################
# - Constants
####################
-log = _logger.get()
-PATH_ADDON_ROOT = Path(__file__).resolve().parent
-with (PATH_ADDON_ROOT / 'pyproject.toml').open('rb') as f:
- PROJ_SPEC = tomllib.load(f)
+log = simple_logger.get(__name__)
####################
# - Preferences
####################
-class BlenderMaxwellAddonPreferences(bpy.types.AddonPreferences):
- bl_idname = PROJ_SPEC['project']['name'] ## MUST match addon package name
+class BLMaxwellAddonPrefs(bpy.types.AddonPreferences):
+ """Manages user preferences and settings for the Blender Maxwell addon.
+ """
+ bl_idname = info.ADDON_NAME ## MUST match addon package name
####################
# - Properties
####################
- # Default PyDeps Path
- use_default_path_addon_pydeps: bpy.props.BoolProperty(
+ # Use of Default PyDeps Path
+ use_default_pydeps_path: bpy.props.BoolProperty(
name='Use Default PyDeps Path',
description='Whether to use the default PyDeps path',
default=True,
- update=lambda self, context: self.sync_use_default_path_addon_pydeps(
- context
- ),
+ update=lambda self, context: self.sync_use_default_pydeps_path(context),
)
- cache_path_addon_pydeps: bpy.props.StringProperty(
+ cache__pydeps_path_while_using_default: bpy.props.StringProperty(
name='Cached Addon PyDeps Path',
- default=(_default_pydeps_path := str(pydeps.DEFAULT_PATH_DEPS)),
- ) ## Cache for use when toggling use of default pydeps path.
- ## Must default to same as raw_path_* if default=True on use_default_*
+ default=(_default_pydeps_path := str(info.DEFAULT_PATH_DEPS)),
+ )
# Custom PyDeps Path
- raw_path_addon_pydeps: bpy.props.StringProperty(
+ bl__pydeps_path: bpy.props.StringProperty(
name='Addon PyDeps Path',
description='Path to Addon Python Dependencies',
subtype='FILE_PATH',
default=_default_pydeps_path,
- update=lambda self, context: self.sync_path_addon_pydeps(context),
+ update=lambda self, _: self.sync_pydeps_path(),
)
- prev_raw_path_addon_pydeps: bpy.props.StringProperty(
+ cache__backup_pydeps_path: bpy.props.StringProperty(
name='Previous Addon PyDeps Path',
default=_default_pydeps_path,
- ) ## Use to restore raw_path_addon_pydeps after non-validated change.
+ )
+
+ # Log Settings
+ use_log_console: bpy.props.BoolProperty(
+ name='Log to Console',
+ description='Whether to use the console for addon logging',
+ default=True,
+ update=lambda self, _: self.sync_addon_logging(),
+ )
+ bl__log_level_console: bpy.props.EnumProperty(
+ name='Console Log Level',
+ description='Level of addon logging to expose in the console',
+ items=[
+ ('DEBUG', 'Debug', 'Debug'),
+ ('INFO', 'Info', 'Info'),
+ ('WARNING', 'Warning', 'Warning'),
+ ('ERROR', 'Error', 'Error'),
+ ('CRITICAL', 'Critical', 'Critical'),
+ ],
+ default='DEBUG',
+ update=lambda self, _: self.sync_addon_logging(),
+ )
+
+ use_log_file: bpy.props.BoolProperty(
+ name='Log to File',
+ description='Whether to use a file for addon logging',
+ default=True,
+ update=lambda self, _: self.sync_addon_logging(),
+ )
+ bl__log_file_path: bpy.props.StringProperty(
+ name='Log Path',
+ description='Path to the Addon Log File',
+ subtype='FILE_PATH',
+ default=str(info.DEFAULT_LOG_PATH),
+ update=lambda self, _: self.sync_addon_logging(),
+ )
+ bl__log_level_file: bpy.props.EnumProperty(
+ name='File Log Level',
+ description='Level of addon logging to expose in the file',
+ items=[
+ ('DEBUG', 'Debug', 'Debug'),
+ ('INFO', 'Info', 'Info'),
+ ('WARNING', 'Warning', 'Warning'),
+ ('ERROR', 'Error', 'Error'),
+ ('CRITICAL', 'Critical', 'Critical'),
+ ],
+ default='DEBUG',
+ update=lambda self, _: self.sync_addon_logging(),
+ )
# TODO: LOGGING SETTINGS
- ####################
- # - Property Sync
- ####################
- def sync_use_default_path_addon_pydeps(self, _: bpy.types.Context):
- # Switch to Default
- if self.use_default_path_addon_pydeps:
- self.cache_path_addon_pydeps = self.raw_path_addon_pydeps
- self.raw_path_addon_pydeps = str(
- pydeps.DEFAULT_PATH_DEPS.resolve()
- )
-
- # Switch from Default
- else:
- self.raw_path_addon_pydeps = self.cache_path_addon_pydeps
- self.cache_path_addon_pydeps = ''
-
- def sync_path_addon_pydeps(self, _: bpy.types.Context):
- # Error if Default Path is in Use
- if self.use_default_path_addon_pydeps:
- self.raw_path_addon_pydeps = self.prev_raw_path_addon_pydeps
- msg = "Can't update pydeps path while default path is being used"
- raise ValueError(msg)
-
- # Error if Dependencies are All Installed
- if pydeps.DEPS_OK:
- self.raw_path_addon_pydeps = self.prev_raw_path_addon_pydeps
- msg = "Can't update pydeps path while dependencies are installed"
- raise ValueError(msg)
-
- # Update PyDeps
- ## This also updates pydeps.DEPS_OK and pydeps.DEPS_ISSUES.
- ## The result is used to run any delayed registrations...
- ## ...which might be waiting for deps to be satisfied.
- if pydeps.check_pydeps(self.path_addon_pydeps):
- registration.run_delayed_registration(
- registration.EVENT__DEPS_SATISFIED,
- self.path_addon_pydeps,
- )
- self.prev_raw_path_addon_pydeps = self.raw_path_addon_pydeps
-
####################
# - Property Methods
####################
@property
- def path_addon_pydeps(self) -> Path:
- return Path(bpy.path.abspath(self.raw_path_addon_pydeps))
+ def pydeps_path(self) -> Path:
+ return Path(bpy.path.abspath(self.bl__pydeps_path))
- @path_addon_pydeps.setter
- def path_addon_pydeps(self, value: Path) -> None:
- self.raw_path_addon_pydeps = str(value.resolve())
+ @pydeps_path.setter
+ def pydeps_path(self, value: Path) -> None:
+ self.bl__pydeps_path = str(value.resolve())
+
+ @property
+ def log_path(self) -> Path:
+ return Path(bpy.path.abspath(self.bl__log_file_path))
+
+ ####################
+ # - Property Sync
+ ####################
+ def sync_addon_logging(self, only_sync_logger: logging.Logger | None = None):
+ if pydeps.DEPS_OK:
+ log.info('Getting Logger (DEPS_OK = %s)', str(pydeps.DEPS_OK))
+ with pydeps.importable_addon_deps(self.pydeps_path):
+ from .utils import logger
+ else:
+ log.info('Getting Simple Logger (DEPS_OK = %s)', str(pydeps.DEPS_OK))
+ logger = simple_logger
+
+ # Retrieve Configured Log Levels
+ log_level_console = logger.LOG_LEVEL_MAP[self.bl__log_level_console]
+ log_level_file = logger.LOG_LEVEL_MAP[self.bl__log_level_file]
+
+ log_setup_kwargs = {
+ 'console_level': log_level_console if self.use_log_console else None,
+ 'file_path': self.log_path if self.use_log_file else None,
+ 'file_level': log_level_file,
+ }
+
+ # Sync Single Logger / All Loggers
+ if only_sync_logger is not None:
+ logger.setup_logger(
+ logger.console_handler,
+ logger.file_handler,
+ only_sync_logger,
+ **log_setup_kwargs,
+ )
+ return
+ logger.sync_loggers(
+ logger.console_handler,
+ logger.file_handler,
+ **log_setup_kwargs,
+ )
+
+ def sync_use_default_pydeps_path(self, _: bpy.types.Context):
+ # Switch to Default
+ if self.use_default_pydeps_path:
+ log.info(
+ 'Switching to Default PyDeps Path %s',
+ str(info.DEFAULT_PATH_DEPS.resolve()),
+ )
+ self.cache__pydeps_path_while_using_default = self.bl__pydeps_path
+ self.bl__pydeps_path = str(info.DEFAULT_PATH_DEPS.resolve())
+
+ # Switch from Default
+ else:
+ log.info(
+ 'Switching from Default PyDeps Path %s to Cached PyDeps Path %s',
+ str(info.DEFAULT_PATH_DEPS.resolve()),
+ self.cache__pydeps_path_while_using_default,
+ )
+ self.bl__pydeps_path = self.cache__pydeps_path_while_using_default
+ self.cache__pydeps_path_while_using_default = ''
+
+ def sync_pydeps_path(self):
+ if self.cache__backup_pydeps_path != self.bl__pydeps_path:
+ log.info(
+ 'Syncing PyDeps Path from/to: %s => %s',
+ self.cache__backup_pydeps_path,
+ self.bl__pydeps_path,
+ )
+ else:
+ log.info(
+ 'Syncing PyDeps Path In-Place @ %s',
+ str(self.bl__pydeps_path),
+ )
+
+ # Error: Default Path in Use
+ if self.use_default_pydeps_path:
+ self.bl__pydeps_path = self.cache__backup_pydeps_path
+ msg = "Can't update pydeps path while default path is being used"
+ raise ValueError(msg)
+
+ # Error: PyDeps Already Installed
+ if pydeps.DEPS_OK:
+ self.bl__pydeps_path = self.cache__backup_pydeps_path
+ msg = "Can't update pydeps path while dependencies are installed"
+ raise ValueError(msg)
+
+ # Re-Check PyDeps
+ log.info(
+ 'Checking PyDeps of New Path %s',
+ str(self.pydeps_path),
+ )
+ if pydeps.check_pydeps(self.pydeps_path):
+ # Re-Sync Loggers
+ ## We can now upgrade to the fancier loggers.
+ self.sync_addon_logging()
+
+ # Run Delayed Registrations
+ ## Since the deps are OK, we can now register the whole addon.
+ registration.run_delayed_registration(
+ registration.EVENT__DEPS_SATISFIED,
+ self.pydeps_path,
+ )
+
+ # Backup New PyDeps Path
+ self.cache__backup_pydeps_path = self.bl__pydeps_path
####################
# - UI
####################
def draw(self, _: bpy.types.Context) -> None:
layout = self.layout
- num_pydeps_issues = (
- len(pydeps.DEPS_ISSUES) if pydeps.DEPS_ISSUES is not None else 0
- )
+ num_pydeps_issues = len(pydeps.DEPS_ISSUES) if pydeps.DEPS_ISSUES else 0
+
+ # Box w/Split: Log Level
+ box = layout.box()
+ row = box.row()
+ row.alignment = 'CENTER'
+ row.label(text='Logging')
+ split = box.split(factor=0.5)
+
+ ## Split Col: Console Logging
+ col = split.column()
+ row = col.row()
+ row.prop(self, 'use_log_console', toggle=True)
+
+ row = col.row()
+ row.enabled = self.use_log_console
+ row.prop(self, 'bl__log_level_console')
+
+ ## Split Col: File Logging
+ col = split.column()
+ row = col.row()
+ row.prop(self, 'use_log_file', toggle=True)
+
+ row = col.row()
+ row.enabled = self.use_log_file
+ row.prop(self, 'bl__log_file_path')
+
+ row = col.row()
+ row.enabled = self.use_log_file
+ row.prop(self, 'bl__log_level_file')
# Box: Dependency Status
box = layout.box()
## Row: Header
row = box.row(align=True)
row.alignment = 'CENTER'
- row.label(text='Addon-Specific Python Deps')
+ row.label(text='Python Dependencies')
## Row: Toggle Default PyDeps Path
row = box.row(align=True)
row.enabled = not pydeps.DEPS_OK
row.prop(
self,
- 'use_default_path_addon_pydeps',
+ 'use_default_pydeps_path',
text='Use Default PyDeps Install Path',
toggle=True,
)
## Row: Current PyDeps Path
row = box.row(align=True)
- row.enabled = (
- not pydeps.DEPS_OK and not self.use_default_path_addon_pydeps
- )
- row.prop(self, 'raw_path_addon_pydeps', text='PyDeps Install Path')
+ row.enabled = not pydeps.DEPS_OK and not self.use_default_pydeps_path
+ row.prop(self, 'bl__pydeps_path', text='PyDeps Install Path')
## Row: More Information Panel
- row = box.row(align=True)
- header, panel = row.panel('pydeps_issues', default_closed=True)
- header.label(text=f'Dependency Conflicts ({num_pydeps_issues})')
+ col = box.column(align=True)
+ header, panel = col.panel('pydeps_issues', default_closed=True)
+ header.label(text=f'Install Mismatches ({num_pydeps_issues})')
if panel is not None:
grid = panel.grid_flow()
for issue in pydeps.DEPS_ISSUES:
@@ -151,27 +285,25 @@ class BlenderMaxwellAddonPreferences(bpy.types.AddonPreferences):
## Row: Install
row = box.row(align=True)
- row.enabled = not pydeps.DEPS_OK
op = row.operator(
install_deps.InstallPyDeps.bl_idname,
text='Install PyDeps',
)
- op.path_addon_pydeps = str(self.path_addon_pydeps)
- op.path_addon_reqs = str(pydeps.PATH_REQS)
+ op.path_addon_pydeps = str(self.pydeps_path)
+ op.path_addon_reqs = str(info.PATH_REQS)
## Row: Uninstall
row = box.row(align=True)
- row.enabled = pydeps.DEPS_OK
op = row.operator(
uninstall_deps.UninstallPyDeps.bl_idname,
text='Uninstall PyDeps',
)
- op.path_addon_pydeps = str(self.path_addon_pydeps)
+ op.path_addon_pydeps = str(self.pydeps_path)
####################
# - Blender Registration
####################
BL_REGISTER = [
- BlenderMaxwellAddonPreferences,
+ BLMaxwellAddonPrefs,
]
diff --git a/src/blender_maxwell/registration.py b/src/blender_maxwell/registration.py
index 3d4200a..81b6a3d 100644
--- a/src/blender_maxwell/registration.py
+++ b/src/blender_maxwell/registration.py
@@ -3,9 +3,9 @@ from pathlib import Path
import bpy
-from .utils import logger as _logger
+from .nodeps.utils import simple_logger
-log = _logger.get()
+log = simple_logger.get(__name__)
# TODO: More types for these things!
DelayedRegKey: typ.TypeAlias = str
@@ -33,18 +33,28 @@ EVENT__DEPS_SATISFIED: str = 'on_deps_satisfied'
# - Class Registration
####################
def register_classes(bl_register: list):
+ log.info('Registering %s Classes', len(bl_register))
for cls in bl_register:
if cls.bl_idname in REG__CLASSES:
msg = f'Skipping register of {cls.bl_idname}'
log.info(msg)
continue
+ log.debug(
+ 'Registering Class %s',
+ repr(cls),
+ )
bpy.utils.register_class(cls)
REG__CLASSES.append(cls)
def unregister_classes():
+ log.info('Unregistering %s Classes', len(REG__CLASSES))
for cls in reversed(REG__CLASSES):
+ log.debug(
+ 'Unregistering Class %s',
+ repr(cls),
+ )
bpy.utils.unregister_class(cls)
REG__CLASSES.clear()
@@ -61,8 +71,13 @@ def register_keymap_items(keymap_item_defs: list[dict]):
name='Node Editor',
space_type='NODE_EDITOR',
)
+ log.info(
+ 'Registered Keymap %s',
+ str(BL_KEYMAP),
+ )
# Register Keymaps
+ log.info('Registering %s Keymap Items', len(keymap_item_defs))
for keymap_item_def in keymap_item_defs:
keymap_item = BL_KEYMAP.keymap_items.new(
*keymap_item_def['_'],
@@ -70,6 +85,11 @@ def register_keymap_items(keymap_item_defs: list[dict]):
shift=keymap_item_def['shift'],
alt=keymap_item_def['alt'],
)
+ log.debug(
+ 'Registered Keymap Item %s with spec %s',
+ repr(keymap_item),
+ keymap_item_def,
+ )
REG__KEYMAP_ITEMS.append(keymap_item)
@@ -77,11 +97,20 @@ def unregister_keymap_items():
global BL_KEYMAP # noqa: PLW0603
# Unregister Keymaps
+ log.info('Unregistering %s Keymap Items', len(REG__KEYMAP_ITEMS))
for keymap_item in reversed(REG__KEYMAP_ITEMS):
+ log.debug(
+ 'Unregistered Keymap Item %s',
+ repr(keymap_item),
+ )
BL_KEYMAP.keymap_items.remove(keymap_item)
# Lazy-Unload BL_NODE_KEYMAP
if BL_KEYMAP is not None:
+ log.info(
+ 'Unregistered Keymap %s',
+ repr(BL_KEYMAP),
+ )
REG__KEYMAP_ITEMS.clear()
BL_KEYMAP = None
@@ -99,6 +128,11 @@ def delay_registration(
raise ValueError(msg)
def register_cb(path_deps: Path):
+ log.info(
+ 'Running Delayed Registration (key %s) with PyDeps: %s',
+ delayed_reg_key,
+ path_deps,
+ )
register_classes(classes_cb(path_deps))
register_keymap_items(keymap_item_defs_cb(path_deps))
diff --git a/src/blender_maxwell/services/__init__.py b/src/blender_maxwell/services/__init__.py
new file mode 100644
index 0000000..54f247c
--- /dev/null
+++ b/src/blender_maxwell/services/__init__.py
@@ -0,0 +1,3 @@
+from . import tdcloud
+
+__all__ = ['tdcloud']
diff --git a/src/blender_maxwell/utils/tdcloud.py b/src/blender_maxwell/services/tdcloud.py
similarity index 91%
rename from src/blender_maxwell/utils/tdcloud.py
rename to src/blender_maxwell/services/tdcloud.py
index ee80376..5dfa553 100644
--- a/src/blender_maxwell/utils/tdcloud.py
+++ b/src/blender_maxwell/services/tdcloud.py
@@ -30,17 +30,16 @@ IS_AUTHENTICATED = False
def is_online():
- global IS_ONLINE
return IS_ONLINE
def set_online():
- global IS_ONLINE
+ global IS_ONLINE # noqa: PLW0603
IS_ONLINE = True
def set_offline():
- global IS_ONLINE
+ global IS_ONLINE # noqa: PLW0603
IS_ONLINE = False
@@ -48,8 +47,7 @@ def set_offline():
# - Cloud Authentication
####################
def check_authentication() -> bool:
- global IS_AUTHENTICATED
- global IS_ONLINE
+ global IS_AUTHENTICATED # noqa: PLW0603
# Check Previous Authentication
## If we authenticated once, we presume that it'll work again.
@@ -95,10 +93,10 @@ class TidyCloudFolders:
try:
cloud_folders = td_web.core.task_core.Folder.list()
set_online()
- except td.exceptions.WebError:
+ except td.exceptions.WebError as ex:
set_offline()
msg = 'Tried to get cloud folders, but cannot connect to cloud'
- raise RuntimeError(msg)
+ raise RuntimeError(msg) from ex
folders = {
cloud_folder.folder_id: cloud_folder
@@ -117,12 +115,12 @@ class TidyCloudFolders:
try:
cloud_folder = td_web.core.task_core.Folder.create(folder_name)
set_online()
- except td.exceptions.WebError:
+ except td.exceptions.WebError as ex:
set_offline()
msg = (
'Tried to create cloud folder, but cannot connect to cloud'
)
- raise RuntimeError(msg)
+ raise RuntimeError(msg) from ex
if cls.cache_folders is None:
cls.cache_folders = {}
@@ -185,9 +183,11 @@ class TidyCloudTasks:
- `cloud_task.get_log(path)`: GET the run log. Remember to use `NamedTemporaryFile` if a stringified log is desired.
"""
- cache_tasks: dict[CloudTaskID, CloudTask] = {}
- cache_folder_tasks: dict[CloudFolderID, set[CloudTaskID]] = {}
- cache_task_info: dict[CloudTaskID, CloudTaskInfo] = {}
+ cache_tasks: typ.ClassVar[dict[CloudTaskID, CloudTask]] = {}
+ cache_folder_tasks: typ.ClassVar[
+ dict[CloudFolderID, set[CloudTaskID]]
+ ] = {}
+ cache_task_info: typ.ClassVar[dict[CloudTaskID, CloudTaskInfo]] = {}
@classmethod
def clear_cache(cls):
@@ -217,12 +217,12 @@ class TidyCloudTasks:
try:
folder_tasks = cloud_folder.list_tasks()
set_online()
- except td.exceptions.WebError:
+ except td.exceptions.WebError as ex:
set_offline()
msg = (
'Tried to get tasks of a cloud folder, but cannot access cloud'
)
- raise RuntimeError(msg)
+ raise RuntimeError(msg) from ex
# No Tasks: Empty Set
if folder_tasks is None:
@@ -250,9 +250,7 @@ class TidyCloudTasks:
)
## Task by-Folder Cache
- cls.cache_folder_tasks[cloud_folder.folder_id] = {
- task_id for task_id in cloud_tasks
- }
+ cls.cache_folder_tasks[cloud_folder.folder_id] = set(cloud_tasks)
return cloud_tasks
@@ -287,14 +285,14 @@ class TidyCloudTasks:
folder_name=cloud_folder.folder_name,
)
set_online()
- except td.exceptions.WebError:
+ except td.exceptions.WebError as ex:
set_offline()
msg = 'Tried to create cloud task, but cannot access cloud'
- raise RuntimeError(msg)
+ raise RuntimeError(msg) from ex
# Upload Simulation to Cloud Task
if upload_progress_cb is not None:
- upload_progress_cb = lambda uploaded_bytes: None
+ raise NotImplementedError
try:
cloud_task.upload_simulation(
stub,
@@ -302,10 +300,10 @@ class TidyCloudTasks:
# progress_callback=upload_progress_cb,
)
set_online()
- except td.exceptions.WebError:
+ except td.exceptions.WebError as ex:
set_offline()
msg = 'Tried to upload simulation to cloud task, but cannot access cloud'
- raise RuntimeError(msg)
+ raise RuntimeError(msg) from ex
# Populate Caches
## Direct Task Cache
@@ -348,10 +346,10 @@ class TidyCloudTasks:
try:
cloud_task.delete()
set_online()
- except td.exceptions.WebError:
+ except td.exceptions.WebError as ex:
set_offline()
msg = 'Tried to delete cloud task, but cannot access cloud'
- raise RuntimeError(msg)
+ raise RuntimeError(msg) from ex
# Populate Caches
## Direct Task Cache
@@ -377,7 +375,6 @@ class TidyCloudTasks:
# Repopulate All Caches
## By deleting the folder ID, all tasks within will be reloaded
del cls.cache_folder_tasks[folder_id]
- folder_tasks = cls.tasks(cloud_folder)
return cls.tasks(cloud_folder)[task_id]
@@ -395,10 +392,9 @@ class TidyCloudTasks:
# Repopulate All Caches
## By deleting the folder ID, all tasks within will be reloaded
del cls.cache_folder_tasks[folder_id]
- folder_tasks = cls.tasks(cloud_folder)
return {
- task_id: cls.cache_tasks[task_id]
+ task_id: cls.tasks(cloud_folder)[task_id]
for task_id in cls.cache_folder_tasks[folder_id]
}
@@ -410,9 +406,9 @@ class TidyCloudTasks:
try:
new_cloud_task.abort()
set_online()
- except td.exceptions.WebError:
+ except td.exceptions.WebError as ex:
set_offline()
msg = 'Tried to abort cloud task, but cannot access cloud'
- raise RuntimeError(msg)
+ raise RuntimeError(msg) from ex
return cls.update_task(cloud_task)
diff --git a/src/blender_maxwell/utils/__init__.py b/src/blender_maxwell/utils/__init__.py
index e69de29..62e1cb9 100644
--- a/src/blender_maxwell/utils/__init__.py
+++ b/src/blender_maxwell/utils/__init__.py
@@ -0,0 +1,17 @@
+from ..nodeps.utils import pydeps
+from . import (
+ analyze_geonodes,
+ blender_type_enum,
+ extra_sympy_units,
+ logger,
+ pydantic_sympy,
+)
+
+__all__ = [
+ 'pydeps',
+ 'analyze_geonodes',
+ 'blender_type_enum',
+ 'extra_sympy_units',
+ 'logger',
+ 'pydantic_sympy',
+]
diff --git a/src/blender_maxwell/utils/analyze_geonodes.py b/src/blender_maxwell/utils/analyze_geonodes.py
index 91a9ef3..e236ac0 100644
--- a/src/blender_maxwell/utils/analyze_geonodes.py
+++ b/src/blender_maxwell/utils/analyze_geonodes.py
@@ -6,7 +6,7 @@ INVALID_BL_SOCKET_TYPES = {
def interface(
- geo_nodes,
+ geo_nodes, ## TODO: bpy type
direc: typx.Literal['INPUT', 'OUTPUT'],
):
"""Returns 'valid' GeoNodes interface sockets, meaning that:
diff --git a/src/blender_maxwell/utils/blender_type_enum.py b/src/blender_maxwell/utils/blender_type_enum.py
index 3c8a086..74c5b06 100644
--- a/src/blender_maxwell/utils/blender_type_enum.py
+++ b/src/blender_maxwell/utils/blender_type_enum.py
@@ -2,7 +2,7 @@ import enum
class BlenderTypeEnum(str, enum.Enum):
- def _generate_next_value_(name, start, count, last_values):
+ def _generate_next_value_(name, *_):
return name
diff --git a/src/blender_maxwell/utils/extra_sympy_units.py b/src/blender_maxwell/utils/extra_sympy_units.py
index cb34274..2e7b932 100644
--- a/src/blender_maxwell/utils/extra_sympy_units.py
+++ b/src/blender_maxwell/utils/extra_sympy_units.py
@@ -1,7 +1,10 @@
import functools
-import sympy as sp
-import sympy.physics.units as spu
+from . import pydeps
+
+with pydeps.syspath_from_bpy_prefs():
+ import sympy as sp
+ import sympy.physics.units as spu
####################
diff --git a/src/blender_maxwell/utils/logger.py b/src/blender_maxwell/utils/logger.py
index 9791757..efd6bef 100644
--- a/src/blender_maxwell/utils/logger.py
+++ b/src/blender_maxwell/utils/logger.py
@@ -1,31 +1,66 @@
import logging
+from pathlib import Path
-LOGGER = logging.getLogger('blender_maxwell')
+import rich.console
+import rich.logging
+
+from .. import info
+from ..nodeps.utils import simple_logger
+from ..nodeps.utils.simple_logger import (
+ LOG_LEVEL_MAP, # noqa: F401
+ LogLevel,
+ loggers, # noqa: F401
+ setup_logger, # noqa: F401
+ simple_loggers, # noqa: F401
+ sync_loggers, # noqa: F401
+)
-def get():
- if LOGGER is None:
- # Set Sensible Defaults
- LOGGER.setLevel(logging.DEBUG)
- # FORMATTER = logging.Formatter(
- # '%(asctime)-15s %(levelname)8s %(name)s %(message)s'
- # )
-
- # Add Stream Handler
- STREAM_HANDLER = logging.StreamHandler()
- # STREAM_HANDLER.setFormatter(FORMATTER)
- LOGGER.addHandler(STREAM_HANDLER)
-
- return LOGGER
+####################
+# - Logging Handlers
+####################
+def console_handler(level: LogLevel) -> rich.logging.RichHandler:
+ rich_formatter = logging.Formatter(
+ '%(message)s',
+ datefmt='[%X]',
+ )
+ rich_handler = rich.logging.RichHandler(
+ level=level,
+ console=rich.console.Console(
+ color_system='truecolor', stderr=True
+ ), ## TODO: Should be 'auto'; bl_run.py hijinks are interfering
+ # console=rich.console.Console(stderr=True),
+ rich_tracebacks=True,
+ )
+ rich_handler.setFormatter(rich_formatter)
+ return rich_handler
-def set_level(level):
- LOGGER.setLevel(level)
+def file_handler(
+ path_log_file: Path, level: LogLevel
+) -> rich.logging.RichHandler:
+ return simple_logger.file_handler(path_log_file, level)
-def enable_logfile():
- raise NotImplementedError
+####################
+# - Logger Setup
+####################
+def get(module_name):
+ logger = logging.getLogger(module_name)
+
+ # Setup Logger from Addon Preferences
+ if (addon_prefs := info.addon_prefs()) is None:
+ msg = 'Addon preferences not defined'
+ raise RuntimeError(msg)
+ addon_prefs.sync_addon_logging(only_sync_logger=logger)
+
+ return logger
-def disable_logfile():
- raise NotImplementedError
+####################
+# - Logger Sync
+####################
+#def upgrade_simple_loggers():
+# """Upgrades simple loggers to rich-enabled loggers."""
+# for logger in simple_loggers():
+# setup_logger(console_handler, file_handler, logger)
diff --git a/scripts/bl_run.py b/src/scripts/bl_run.py
similarity index 86%
rename from scripts/bl_run.py
rename to src/scripts/bl_run.py
index 5206550..a2461b2 100644
--- a/scripts/bl_run.py
+++ b/src/scripts/bl_run.py
@@ -3,6 +3,7 @@
See
"""
+import logging
import shutil
import sys
import traceback
@@ -10,9 +11,17 @@ from pathlib import Path
import bpy
-sys.path.insert(0, str(Path(__file__).resolve().parent))
-import info
-import pack
+PATH_SCRIPT = str(Path(__file__).resolve().parent)
+sys.path.insert(0, str(PATH_SCRIPT))
+import info # noqa: E402
+import pack # noqa: E402
+
+sys.path.remove(str(PATH_SCRIPT))
+
+# Set Bootstrap Log Level
+## This will be the log-level of both console and file logs, at first...
+## ...until the addon preferences have been loaded.
+BOOTSTRAP_LOG_LEVEL = logging.DEBUG
## TODO: Preferences item that allows using BLMaxwell 'starter.blend' as Blender's default starter blendfile.
@@ -113,19 +122,22 @@ def install_addon(addon_name: str, addon_zip: Path) -> None:
msg = f"Couldn't enable addon {addon_name}"
raise RuntimeError(msg)
- # Set Dev Path for Addon Dependencies
- addon_prefs = bpy.context.preferences.addons[addon_name].preferences
- addon_prefs.use_default_path_addon_pydeps = False
- addon_prefs.path_addon_pydeps = info.PATH_ADDON_DEV_DEPS
-
# Save User Preferences
bpy.ops.wm.save_userpref()
+def setup_for_development(addon_name: str, path_addon_dev_deps: Path) -> None:
+ addon_prefs = bpy.context.preferences.addons[addon_name].preferences
+
+ # PyDeps Path
+ addon_prefs.use_default_pydeps_path = False
+ addon_prefs.pydeps_path = path_addon_dev_deps
+
+
####################
# - Entrypoint
####################
-if __name__ == '__main__':
+def main():
# Delete Addon (maybe; possibly restart)
delete_addon_if_loaded(info.ADDON_NAME)
@@ -139,6 +151,7 @@ if __name__ == '__main__':
info.PATH_ADDON_ZIP,
info.PATH_ROOT / 'pyproject.toml',
info.PATH_ROOT / 'requirements.lock',
+ initial_log_level=BOOTSTRAP_LOG_LEVEL,
) as path_zipped:
try:
install_addon(info.ADDON_NAME, path_zipped)
@@ -146,6 +159,9 @@ if __name__ == '__main__':
traceback.print_exc()
install_failed = True
+ # Setup Addon for Development Use
+ setup_for_development(info.ADDON_NAME, info.PATH_ADDON_DEV_DEPS)
+
# Load Development .blend
## TODO: We need a better (also final-deployed-compatible) solution for what happens when a user opened a .blend file without installing dependencies!
if not install_failed:
@@ -153,3 +169,7 @@ if __name__ == '__main__':
else:
bpy.ops.wm.quit_blender()
sys.exit(info.STATUS_NOINSTALL_ADDON)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/scripts/run.py b/src/scripts/dev.py
similarity index 95%
rename from scripts/run.py
rename to src/scripts/dev.py
index 58a8041..048d9cf 100644
--- a/scripts/run.py
+++ b/src/scripts/dev.py
@@ -1,3 +1,4 @@
+# noqa: INP001
import os
import subprocess
from pathlib import Path
@@ -43,7 +44,7 @@ def run_blender(py_script: Path, print_live: bool = False):
####################
# - Run Blender w/Clean Addon Reinstall
####################
-if __name__ == '__main__':
+def main():
return_code, output = run_blender(info.PATH_BL_RUN, print_live=False)
if return_code == info.STATUS_UNINSTALLED_ADDON:
return_code, output = run_blender(info.PATH_BL_RUN, print_live=True)
@@ -52,3 +53,6 @@ if __name__ == '__main__':
raise ValueError(msg)
elif return_code != 0:
print(''.join(output)) # noqa: T201
+
+if __name__ == "__main__":
+ main()
diff --git a/scripts/info.py b/src/scripts/info.py
similarity index 86%
rename from scripts/info.py
rename to src/scripts/info.py
index 32b0288..572626a 100644
--- a/scripts/info.py
+++ b/src/scripts/info.py
@@ -1,9 +1,9 @@
import tomllib
from pathlib import Path
-PATH_ROOT = Path(__file__).resolve().parent.parent
-PATH_RUN = PATH_ROOT / 'scripts' / 'run.py'
-PATH_BL_RUN = PATH_ROOT / 'scripts' / 'bl_run.py'
+PATH_ROOT = Path(__file__).resolve().parent.parent.parent
+PATH_SRC = PATH_ROOT / 'src'
+PATH_BL_RUN = PATH_SRC / 'scripts' / 'bl_run.py'
PATH_BUILD = PATH_ROOT / 'build'
PATH_BUILD.mkdir(exist_ok=True)
@@ -41,6 +41,8 @@ PATH_ADDON_ZIP = (
PATH_ADDON_BLEND_STARTER = PATH_ADDON_PKG / 'blenders' / 'starter.blend'
+BOOTSTRAP_LOG_LEVEL_FILENAME = '.bootstrap_log_level'
+
# Install the ZIPped Addon
####################
# - Development Information
diff --git a/scripts/pack.py b/src/scripts/pack.py
similarity index 68%
rename from scripts/pack.py
rename to src/scripts/pack.py
index e4664ef..179333c 100644
--- a/scripts/pack.py
+++ b/src/scripts/pack.py
@@ -1,4 +1,7 @@
+# noqa: INP001
+
import contextlib
+import logging
import tempfile
import typing as typ
import zipfile
@@ -6,6 +9,8 @@ from pathlib import Path
import info
+LogLevel: typ.TypeAlias = int
+
_PROJ_VERSION_STR = str(
tuple(int(el) for el in info.PROJ_SPEC['project']['version'].split('.'))
)
@@ -18,12 +23,14 @@ BL_INFO_REPLACEMENTS = {
@contextlib.contextmanager
-def zipped_addon(
+def zipped_addon( # noqa: PLR0913
path_addon_pkg: Path,
path_addon_zip: Path,
path_pyproject_toml: Path,
path_requirements_lock: Path,
+ initial_log_level: LogLevel = logging.INFO,
replace_if_exists: bool = False,
+ remove_after_close: bool = True,
) -> typ.Iterator[Path]:
"""Context manager exposing a folder as a (temporary) zip file.
The .zip file is deleted afterwards.
@@ -69,25 +76,43 @@ def zipped_addon(
# Install pyproject.toml @ /pyproject.toml of Addon
f_zip.write(
path_pyproject_toml,
- str(
- (Path(path_addon_pkg.name) / Path(path_pyproject_toml.name))
- .with_suffix('')
- .with_suffix('.toml')
- ),
+ str(Path(path_addon_pkg.name) / Path(path_pyproject_toml.name)),
)
# Install requirements.lock @ /requirements.txt of Addon
f_zip.write(
path_requirements_lock,
- str(
- (Path(path_addon_pkg.name) / Path(path_requirements_lock.name))
- .with_suffix('')
- .with_suffix('.txt')
- ),
+ str(Path(path_addon_pkg.name) / Path(path_requirements_lock.name)),
+ )
+
+ # Set Initial Log-Level
+ f_zip.writestr(
+ str(Path(path_addon_pkg.name) / info.BOOTSTRAP_LOG_LEVEL_FILENAME),
+ str(initial_log_level),
)
# Delete the ZIP
try:
yield path_addon_zip
finally:
- path_addon_zip.unlink()
+ if remove_after_close:
+ path_addon_zip.unlink()
+
+
+####################
+# - Run Blender w/Clean Addon Reinstall
+####################
+def main():
+ with zipped_addon(
+ path_addon_pkg=info.PATH_ADDON_PKG,
+ path_addon_zip=info.PATH_ADDON_ZIP,
+ path_pyproject_toml=info.PATH_ROOT / 'pyproject.toml',
+ path_requirements_lock=info.PATH_ROOT / 'requirements.lock',
+ replace_if_exists=True,
+ remove_after_close=False,
+ ):
+ # TODO: GPG signature for distribution
+ pass
+
+if __name__ == "__main__":
+ main()