Skip to content

Commit

Permalink
Merge branch 'v2.0.0' into crnh/v2.0.0/refactor_core
Browse files Browse the repository at this point in the history
  • Loading branch information
crnh committed Feb 7, 2025
2 parents cb8146f + 858a2ec commit a57fd3f
Show file tree
Hide file tree
Showing 33 changed files with 1,183 additions and 162 deletions.
23 changes: 16 additions & 7 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,17 @@ build-backend = "hatchling.build"
[project]
name = "zospy"
authors = [
{name = "Luc van Vught"},
{name = "Jan-Willem Beenakker"},
{name = "Corné Haasjes"}
{ name = "Luc van Vught" },
{ name = "Jan-Willem Beenakker" },
{ name = "Corné Haasjes" }
]
maintainers = [
{name = "MReye research group", email = "zospy@mreye.nl"}
{ name = "MReye research group", email = "zospy@mreye.nl" }
]

description = "A Python package used to communicate with Zemax OpticStudio through the API"
readme = "README.md"
license = {file = "LICENSE.txt"}
license = { file = "LICENSE.txt" }
keywords = ["Zemax", "OpticStudio", "API", "ZOSAPI"]
classifiers = [
"Development Status :: 5 - Production/Stable",
Expand All @@ -30,7 +30,8 @@ dependencies = [
"pydantic >= 2.4.0",
"numpy",
"semver >= 3.0.0,<4",
"eval_type_backport", # TODO: Remove when dropping support for Python 3.9
"eval_type_backport; python_version <= '3.9'", # TODO: Remove when dropping support for Python 3.9
"typing_extensions; python_version <= '3.10'"
]
dynamic = ["version"]

Expand All @@ -57,6 +58,9 @@ path = "zospy/__init__.py"
[tool.hatch.envs.default]
python = "3.12"
installer = "uv"
path = ".venv"
dependencies = ["pytest"]


[tool.hatch.envs.default.scripts]
test-extension = "hatch test --extension {args}"
Expand Down Expand Up @@ -118,7 +122,6 @@ extend-include = [
exclude = [
"zospy/api/_ZOSAPI",
"zospy/api/_ZOSAPI_constants",

# TODO: Change this when movind the old analyses to zospy.analyses.old
"zospy/analyses/base.py",
"zospy/analyses/extendedscene.py",
Expand Down Expand Up @@ -150,6 +153,12 @@ extend-ignore = [

[tool.ruff.lint.extend-per-file-ignores]
"examples/**" = [
"D", # pydocstyle
"FBT002", # Allow positional boolean arguments
"INP001", # Do not require __init__.py
"RET504",
"S101", # Allow use of assert
"TCH", # Do not require type checking blocks
"T201", # Allow use of print
]
"**/tests/**" = [
Expand Down
24 changes: 23 additions & 1 deletion tests/analyses/new/parsers/test_types.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import gc
import json

import numpy as np
Expand All @@ -6,7 +7,8 @@
from pandas.testing import assert_frame_equal
from pydantic import TypeAdapter, ValidationError

from zospy.analyses.new.parsers.types import ValidatedDataFrame, ValidatedNDArray
from zospy.analyses.new.parsers.types import ValidatedDataFrame, ValidatedNDArray, ZOSAPIConstantAnnotation
from zospy.api import constants

validated_dataframe = TypeAdapter(ValidatedDataFrame)
validated_ndarray = TypeAdapter(ValidatedNDArray)
Expand Down Expand Up @@ -78,3 +80,23 @@ def test_validated_ndarray_invalid_list(self):
with pytest.raises(ValidationError, match="type=invalid_ndarray"):
# List is formatted incorrectly
validated_ndarray.validate_python([[1, 2, 3], [4, 5]])


def _get_zosapi_constant_instances():
return [obj for obj in gc.get_objects() if isinstance(obj, ZOSAPIConstantAnnotation)]


class TestZOSAPIConstant:
@staticmethod
def _hasattr(obj, attr):
for name in attr.split("."):
if not hasattr(obj, name):
return False

obj = getattr(obj, name)

return True

@pytest.mark.parametrize("annotation", _get_zosapi_constant_instances())
def test_constant_exists(self, zos, annotation): # noqa: ARG002
assert self._hasattr(constants, annotation.enum)
29 changes: 29 additions & 0 deletions tests/analyses/new/test_analysis_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import re
from inspect import getsource

import pytest

from zospy.analyses.new.base import BaseAnalysisWrapper, new_analysis
from zospy.analyses.new.systemviewers.base import SystemViewerWrapper
from zospy.api import constants
from zospy.api.constants import process_constant

analysis_wrapper_classes = BaseAnalysisWrapper.__subclasses__()
analysis_wrapper_classes.remove(SystemViewerWrapper)

REGEX_SETTING = re.compile(r"\s*self\.analysis\.Settings\.(?P<setting>\w+)")


@pytest.mark.parametrize("analysis_wrapper", analysis_wrapper_classes)
def test_settings_exist(empty_system, analysis_wrapper):
if analysis_wrapper.MODE == "Nonsequential":
empty_system.make_nonsequential()

analysis = new_analysis(empty_system, process_constant(constants.Analysis.AnalysisIDM, analysis_wrapper.TYPE))
source = getsource(analysis_wrapper.run_analysis)

settings = REGEX_SETTING.findall(source)

nonexistent_settings = [setting for setting in settings if not hasattr(analysis.Settings, setting)]

assert not nonexistent_settings, f"Nonexistent settings: {nonexistent_settings}"
93 changes: 84 additions & 9 deletions tests/analyses/new/test_base.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from __future__ import annotations

import inspect
import json
from dataclasses import fields
Expand All @@ -7,6 +9,7 @@
import numpy as np
import pytest
from pandas import DataFrame
from pydantic import Field
from pydantic.dataclasses import dataclass
from pydantic.fields import FieldInfo

Expand All @@ -15,14 +18,24 @@
AnalysisData,
AnalysisMetadata,
AnalysisResult,
AnalysisSettings,
BaseAnalysisWrapper,
_validated_setter,
)
from zospy.analyses.new.decorators import analysis_settings
from zospy.analyses.new.parsers.types import ValidatedDataFrame
from zospy.analyses.new.reports.surface_data import SurfaceDataSettings
from zospy.analyses.new.systemviewers.base import SystemViewerWrapper


def all_subclasses(cls):
return set(cls.__subclasses__()).union([s for c in cls.__subclasses__() for s in all_subclasses(c)])


analysis_wrapper_classes = all_subclasses(BaseAnalysisWrapper)
analysis_wrapper_classes.remove(SystemViewerWrapper)


class TestValidatedSetter:
class MockSettings:
int_setting: int = 1
Expand Down Expand Up @@ -53,20 +66,16 @@ def test_set_non_existing(self):
settings.non_existing = 2


analysis_wrapper_classes = BaseAnalysisWrapper.__subclasses__()
analysis_wrapper_classes.remove(SystemViewerWrapper)


@dataclass
class MockAnalysisData:
int_data: int = 1
string_data: str = "a"


@dataclass
@analysis_settings
class MockAnalysisSettings:
int_setting: int = 1
string_setting: str = "a"
int_setting: int = Field(default=1, description="An integer setting")
string_setting: str = Field(default="a", description="A string setting")


class MockAnalysis(BaseAnalysisWrapper[MockAnalysisData, MockAnalysisSettings]):
Expand All @@ -75,8 +84,14 @@ class MockAnalysis(BaseAnalysisWrapper[MockAnalysisData, MockAnalysisSettings]):
_needs_config_file = False
_needs_text_output_file = False

def __init__(self, int_setting: int = 1, string_setting: str = "a", *, block_remove_temp_files: bool = False):
super().__init__(MockAnalysisSettings(), locals())
def __init__(
self,
*,
int_setting: int = 1,
string_setting: str = "a",
block_remove_temp_files: bool = False,
):
super().__init__(settings_kws=locals())

self.block_remove_temp_files = block_remove_temp_files

Expand Down Expand Up @@ -112,11 +127,21 @@ def get_settings_defaults(settings_class):

return result

def test_get_settings_type(self):
assert MockAnalysis._settings_type == MockAnalysisSettings # noqa: SLF001

def test_settings_type_is_specified(self):
assert MockAnalysis._settings_type is not AnalysisSettings # noqa: SLF001

@pytest.mark.parametrize("cls", analysis_wrapper_classes)
def test_analyses_correct_analysis_name(self, cls):
assert cls.TYPE is not None
assert hasattr(constants.Analysis.AnalysisIDM, cls.TYPE)

@pytest.mark.parametrize("cls", analysis_wrapper_classes)
def test_init_all_keyword_only_parameters(self, cls):
all(p.kind.name == "KEYWORD_ONLY" for _, p in inspect.signature(cls).parameters.items())

@pytest.mark.parametrize("cls", analysis_wrapper_classes)
def test_init_contains_all_settings(self, cls):
if cls().settings is None:
Expand All @@ -139,6 +164,56 @@ def test_analyses_default_values(self, cls):
assert field_name in init_signature.parameters
assert init_signature.parameters[field_name].default == default_value

def test_change_settings_from_parameters(self):
analysis = MockAnalysis(int_setting=2, string_setting="b")

assert analysis.settings.int_setting == 2
assert analysis.settings.string_setting == "b"

def test_change_settings_from_object(self):
settings = MockAnalysisSettings(int_setting=2, string_setting="b")
analysis = MockAnalysis.with_settings(settings)

assert analysis.settings.int_setting == 2
assert analysis.settings.string_setting == "b"

def test_settings_object_is_copied(self):
settings = MockAnalysisSettings(int_setting=2, string_setting="b")
analysis = MockAnalysis.with_settings(settings)

assert analysis.settings is not settings
assert analysis.settings == settings

def test_update_settings_object(self):
analysis = MockAnalysis(int_setting=1, string_setting="a")

analysis.update_settings(settings=MockAnalysisSettings(int_setting=2, string_setting="b"))

assert analysis.settings.int_setting == 2
assert analysis.settings.string_setting == "b"

def test_update_settings_dictionary(self):
analysis = MockAnalysis(int_setting=1, string_setting="a")

analysis.update_settings(settings_kws={"int_setting": 2, "string_setting": "b"})

assert analysis.settings.int_setting == 2
assert analysis.settings.string_setting == "b"

def test_update_settings_object_and_dictionary(self):
analysis = MockAnalysis(int_setting=1, string_setting="a")

analysis.update_settings(
settings=MockAnalysisSettings(int_setting=2, string_setting="a"), settings_kws={"string_setting": "b"}
)

assert analysis.settings.int_setting == 2
assert analysis.settings.string_setting == "b"

def test_update_settings_no_dataclass_raises_type_error(self):
with pytest.raises(TypeError, match="settings should be a dataclass"):
MockAnalysis().update_settings(settings=123)

@pytest.mark.parametrize(
"temp_file_type,filename",
[
Expand Down
11 changes: 11 additions & 0 deletions tests/analyses/new/test_extendedscene.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from zospy.analyses.new.extendedscene import GeometricImageAnalysis


class TestPolarizationTransmission:
def test_can_run(self, simple_system):
result = GeometricImageAnalysis().run(simple_system)
assert result.data is not None

def test_to_json(self, simple_system):
result = GeometricImageAnalysis().run(simple_system)
assert result.from_json(result.to_json()).to_json() == result.to_json()
11 changes: 11 additions & 0 deletions tests/analyses/new/test_physicaloptics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from zospy.analyses.new.physicaloptics import PhysicalOpticsPropagation


class TestPhysicalOpticsPropagation:
def test_can_run(self, simple_system):
result = PhysicalOpticsPropagation().run(simple_system)
assert result.data is not None

def test_to_json(self, simple_system):
result = PhysicalOpticsPropagation().run(simple_system)
assert result.from_json(result.to_json()).to_json() == result.to_json()
2 changes: 1 addition & 1 deletion tests/analyses/new/test_systemviewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class MockSystemViewerSettings:

class MockSystemViewer(SystemViewerWrapper[MockSystemViewerSettings]):
def __init__(self, *, number: int = 5, settings: TestBase.MockSystemViewerSettings | None = None):
super().__init__(settings or TestBase.MockSystemViewerSettings(), locals())
super().__init__(locals())

def _create_analysis(self, *, settings_first=True): # noqa: ARG002
self._analysis = SimpleNamespace(
Expand Down
14 changes: 13 additions & 1 deletion zospy/analyses/new/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,23 @@
... )
"""

from zospy.analyses.new import mtf, polarization, raysandspots, reports, surface, systemviewers, wavefront
from zospy.analyses.new import (
extendedscene,
mtf,
physicaloptics,
polarization,
raysandspots,
reports,
surface,
systemviewers,
wavefront,
)
from zospy.analyses.new.base import new_analysis

__all__ = (
"extendedscene",
"mtf",
"physicaloptics",
"polarization",
"raysandspots",
"reports",
Expand Down
Loading

0 comments on commit a57fd3f

Please sign in to comment.