# MIT License
#
# Copyright (c) 2026 Heiko Sippel
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
#
#
import logging
from typing import Optional, Dict, Any, Tuple
__version__ = "0.1.0"
__author__ = "Heiko Sippel"
from .basic import (
Orientation,
Direction,
AxialCoordinate,
)
__all__ = [
"init",
"is_initialized",
"get_config",
"Orientation",
"Direction",
"AxialCoordinate",
]
_initialized: bool = False
_config: Dict[str, Any] = {}
# ------------------------------- Logging ---------------------------------------
_pyhexlib_logger = logging.getLogger("pyhexlib")
# Libraries should not configure I/O handlers by default. Attach a NullHandler
# so that logging calls do nothing unless the application configures logging.
if not _pyhexlib_logger.handlers:
_pyhexlib_logger.addHandler(logging.NullHandler())
def _ensure_console_handler(log_level: int) -> None:
"""Attach a console handler when no real handler is configured yet."""
has_real_handler = any(not isinstance(handler, logging.NullHandler) for handler in _pyhexlib_logger.handlers)
if has_real_handler:
_pyhexlib_logger.setLevel(log_level)
return
for handler in list(_pyhexlib_logger.handlers):
_pyhexlib_logger.removeHandler(handler)
handler = logging.StreamHandler()
handler.setLevel(log_level)
handler.setFormatter(logging.Formatter("%(levelname)s:%(name)s:%(message)s"))
_pyhexlib_logger.addHandler(handler)
_pyhexlib_logger.setLevel(log_level)
_pyhexlib_logger.propagate = False
def get_logger(name: str):
if isinstance(name, str) and name.startswith("pyhexlib"):
return logging.getLogger(name)
return logging.getLogger(f"pyhexlib.{name}")
# ------------------------------- Standard font -----------------------------
font = {'name': 'Mono.ttf', 'size': 12}
# -------------------------------- Initialization -------------------------------
[docs]
def init(settings: Optional[Dict[str, Any]] = None,
*,
orientation: Orientation = Orientation.FLAT,
scale: Tuple[float, float] = (1.0, 1.0),
log_level: Optional[int] = None) -> Dict[str, Any]:
global _initialized, _config
if settings is None:
settings = {}
cfg = {"orientation": orientation, "scale": scale, **settings}
if log_level is not None:
_pyhexlib_logger.setLevel(log_level)
cfg["log_level"] = log_level
_config = cfg
_initialized = True
logging.getLogger(__name__).debug("pyhexlib initialized: %s", _config)
return _config
[docs]
def is_initialized() -> bool:
return _initialized
[docs]
def get_config() -> Dict[str, Any]:
return dict(_config)
def get_orientation() -> Orientation:
cfg = get_config()
return cfg.get("orientation", Orientation.FLAT)
def set_orientation(orientation: Orientation) -> None:
global _config, _initialized
if not is_initialized():
init(orientation=orientation)
return
_config["orientation"] = orientation
def get_scale() -> Tuple[float, float,]:
cfg = get_config()
# scale is stored under the key "scale" in the config
return cfg.get("scale", (1.0, 1.0))
def set_scale(scale: Tuple[float, float]) -> None:
"""Set the global scale (sx, sy). If module not initialized yet, call init.
The signature expects a (sx, sy) tuple.
"""
global _config, _initialized
if not is_initialized():
init(scale=scale)
return
_config["scale"] = scale
# -------------- Convenience access methods for orientation and scale -------------------
def __getattr__(name: str):
if name == "sx":
return get_scale()[0]
if name == "sy":
return get_scale()[1]
if name == "is_flat":
return get_orientation() == Orientation.FLAT
if name == "is_pointy":
return get_orientation() == Orientation.POINTY
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
def __dir__() -> list:
# include the dynamic convenience attributes in dir(pyhexlib)
names = list(globals().keys())
names.extend(["sx", "sy", "is_flat", "is_pointy"])
return sorted(set(names))