Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions map2loop/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
ch.setFormatter(formatter)
ch.setLevel(logging.WARNING)
from .project import Project
from .api import Map2LoopAPI
from .version import __version__

import warnings # TODO: convert warnings to logging
Expand Down
81 changes: 81 additions & 0 deletions map2loop/api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"""Public API for map2loop
==========================

This module provides a stable interface to dynamically access classes within the
``map2loop`` package. It exposes a :class:`Map2LoopAPI` facade which can be used
by external applications, e.g. a QGIS plugin, to instantiate map2loop classes
without relying on the internal module layout. Classes are discovered using
Python's introspection facilities the first time the API is used.
"""

from __future__ import annotations

import importlib
import inspect
import pkgutil
import pathlib
from types import ModuleType
from typing import Any, Dict, List, Type


class Map2LoopAPI:
"""Facade exposing map2loop classes.

The API implements a dynamic factory that discovers classes from the
``map2loop`` package. It allows creating instances of classes by name and
retrieving class objects directly. Because discovery happens at runtime,
external clients remain agnostic to refactoring inside the package.
"""

def __init__(self) -> None:
self._class_map: Dict[str, Type[Any]] = {}
self._discover_classes()

# ------------------------------------------------------------------
def _discover_classes(self) -> None:
"""Populate ``_class_map`` with classes found in ``map2loop``."""
package_dir = pathlib.Path(__file__).parent
for module_info in pkgutil.iter_modules([str(package_dir)]):
name = module_info.name
if name.startswith("_") or name == "api":
continue # skip private modules and this file
module = importlib.import_module(f"map2loop.{name}")
self._register_classes(module)

def _register_classes(self, module: ModuleType) -> None:
for attr_name in dir(module):
attr = getattr(module, attr_name)
if inspect.isclass(attr) and attr.__module__ == module.__name__:
self._class_map[attr.__name__] = attr

# ------------------------------------------------------------------
def list_classes(self) -> List[str]:
"""Return names of available classes."""
return sorted(self._class_map.keys())

# ------------------------------------------------------------------
def get_class(self, class_name: str) -> Type[Any]:
"""Return class object by name.

Parameters
----------
class_name:
Name of the class as exposed by the API.
"""
cls = self._class_map.get(class_name)
if cls is None:
# If a new class was added after initialisation, attempt to reload.
self._discover_classes()
cls = self._class_map.get(class_name)
if cls is None:
raise ValueError(f"Class '{class_name}' not found in map2loop package")
return cls

# ------------------------------------------------------------------
def create(self, class_name: str, *args: Any, **kwargs: Any) -> Any:
"""Instantiate a class by name."""
cls = self.get_class(class_name)
return cls(*args, **kwargs)


__all__ = ["Map2LoopAPI"]
60 changes: 60 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import pathlib
from map2loop.project import Project

from map2loop.api import Map2LoopAPI


def test_api_list_and_get_class():
api = Map2LoopAPI()
classes = api.list_classes()
assert "MapData" in classes
mapdata_cls = api.get_class("MapData")
assert mapdata_cls.__name__ == "MapData"


def test_api_create_instance():
api = Map2LoopAPI()
obj = api.create("MapData")
from map2loop.mapdata import MapData

assert isinstance(obj, MapData)


def test_api_create_project(tmp_path):
api = Map2LoopAPI()
import map2loop

bbox = {
"minx": 515687.31005864,
"miny": 7493446.76593407,
"maxx": 562666.860106543,
"maxy": 7521273.57407786,
"base": -3200,
"top": 3000,
}
geology_file = (
pathlib.Path(map2loop.__file__).parent
/ "_datasets" / "geodata_files" / "hamersley" / "geology.geojson"
)
structure_file = (
pathlib.Path(map2loop.__file__).parent
/ "_datasets" / "geodata_files" / "hamersley" / "structures.geojson"
)
dtm_file = (
pathlib.Path(map2loop.__file__).parent
/ "_datasets" / "geodata_files" / "hamersley" / "dtm_rp.tif"
)
config_dict = {
"structure": {"dipdir_column": "azimuth2", "dip_column": "dip"},
"geology": {"unitname_column": "unitname", "alt_unitname_column": "code"},
}
project = api.create(
"Project",
bounding_box=bbox,
working_projection="EPSG:28350",
geology_filename=str(geology_file),
dtm_filename=str(dtm_file),
structure_filename=str(structure_file),
config_dictionary=config_dict,
)
assert isinstance(project, Project)
Loading