Source code for pyhexlib

#  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))