From 4fb893cee77f16367b67c1668c835ee858d4ddf2 Mon Sep 17 00:00:00 2001 From: Marnix <150045289+deltamarnix@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:08:10 +0200 Subject: [PATCH] Singledispatch entrypoints (#38) I was playing with how we could set up functions in a modular fashion without having to add a function in the corresponding class. cattrs mentions the use of single dispatch functions for their structure and unstructure functions. We can do the same for functions like plot(), where the plot function should be seen as a module, and not as part of the class itself. I have combined this functionality with the entry_points function to show that a user or plugin package could in theory also add functionality. For example, a user comes with a specific entry point for plotting a specific package, other than the default way that we normally do. --- flopy4/singledispatch/__init__.py | 0 flopy4/singledispatch/plot.py | 9 ++++ flopy4/singledispatch/plot_int.py | 9 ++++ pixi.lock | 80 +++++++++++++++---------------- pyproject.toml | 4 ++ test/test_singledispatch.py | 62 ++++++++++++++++++++++++ 6 files changed, 124 insertions(+), 40 deletions(-) create mode 100644 flopy4/singledispatch/__init__.py create mode 100644 flopy4/singledispatch/plot.py create mode 100644 flopy4/singledispatch/plot_int.py create mode 100644 test/test_singledispatch.py diff --git a/flopy4/singledispatch/__init__.py b/flopy4/singledispatch/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/flopy4/singledispatch/plot.py b/flopy4/singledispatch/plot.py new file mode 100644 index 0000000..31933b4 --- /dev/null +++ b/flopy4/singledispatch/plot.py @@ -0,0 +1,9 @@ +from functools import singledispatch +from typing import Any + + +@singledispatch +def plot(obj, **kwargs) -> Any: + raise NotImplementedError( + "plot method not implemented for type {}".format(type(obj)) + ) diff --git a/flopy4/singledispatch/plot_int.py b/flopy4/singledispatch/plot_int.py new file mode 100644 index 0000000..40ab777 --- /dev/null +++ b/flopy4/singledispatch/plot_int.py @@ -0,0 +1,9 @@ +from typing import Any + +from flopy4.singledispatch.plot import plot + + +@plot.register +def _(v: int, **kwargs) -> Any: + print(f"Plotting a model with kwargs: {kwargs}") + return v diff --git a/pixi.lock b/pixi.lock index f126416..1cc48eb 100644 --- a/pixi.lock +++ b/pixi.lock @@ -99,9 +99,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h8a93ad2_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-ha82c5b3_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_21.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-hcc2c482_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_22.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/6a/21/5b6702a7f963e95456c0de2d495f67bf5fd62840ac655dc451586d23d39a/attrs-24.2.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/c8/d5/867e75361fc45f6de75fe277dd085627a9db5ebb511a87f27dc1396b5351/cattrs-24.1.2-py3-none-any.whl @@ -483,9 +483,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h8a93ad2_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-ha82c5b3_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_21.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-hcc2c482_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_22.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl @@ -962,9 +962,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h8a93ad2_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-ha82c5b3_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_21.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-hcc2c482_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_22.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl @@ -1417,9 +1417,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h8a93ad2_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-ha82c5b3_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_21.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-hcc2c482_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_22.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl @@ -1873,9 +1873,9 @@ environments: - conda: https://conda.anaconda.org/conda-forge/win-64/tk-8.6.13-h5226925_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024b-hc8b5060_0.conda - conda: https://conda.anaconda.org/conda-forge/win-64/ucrt-10.0.22621.0-h57928b3_1.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h8a93ad2_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-ha82c5b3_21.conda - - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_21.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-hcc2c482_22.conda + - conda: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_22.conda - conda: https://conda.anaconda.org/conda-forge/win-64/xz-5.2.6-h8d14728_0.tar.bz2 - pypi: https://files.pythonhosted.org/packages/9e/ef/7a4f225581a0d7886ea28359179cb861d7fbcdefad29663fc1167b86f69f/anyio-4.6.0-py3-none-any.whl - pypi: https://files.pythonhosted.org/packages/a4/6a/e8a041599e78b6b3752da48000b14c8d1e8a04ded09c88c714ba047f34f5/argon2_cffi-23.1.0-py3-none-any.whl @@ -3049,7 +3049,7 @@ packages: name: flopy4 version: 0.0.1.dev0 path: . - sha256: 6dc11acd173d84f6bfdbbcced580fe6754c11f58eb7898e13670adf97ba8d101 + sha256: 46f0c14db79a4a397d25784553531ba81338f135efd9998bbe756f935d200d25 requires_dist: - attrs - cattrs @@ -7812,55 +7812,55 @@ packages: - kind: conda name: vc version: '14.3' - build: h8a93ad2_21 - build_number: 21 + build: ha32ba9b_22 + build_number: 22 subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-h8a93ad2_21.conda - sha256: f14f5238c2e2516e292af43d91df88f212d769b4853eb46d03291793dcf00da9 - md5: e632a9b865d4b653aa656c9fb4f4817c + url: https://conda.anaconda.org/conda-forge/win-64/vc-14.3-ha32ba9b_22.conda + sha256: 2a47c5bd8bec045959afada7063feacd074ad66b170c1ea92dd139b389fcf8fd + md5: 311c9ba1dfdd2895a8cb08346ff26259 depends: - - vc14_runtime >=14.40.33810 + - vc14_runtime >=14.38.33135 track_features: - vc14 license: BSD-3-Clause license_family: BSD purls: [] - size: 17243 - timestamp: 1725984095174 + size: 17447 + timestamp: 1728400826998 - kind: conda name: vc14_runtime version: 14.40.33810 - build: ha82c5b3_21 - build_number: 21 + build: hcc2c482_22 + build_number: 22 subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-ha82c5b3_21.conda - sha256: c3bf51bff7db39ad7e890dbef1b1026df0af36975aea24dea7c5fe1e0b382c40 - md5: b3ebb670caf046e32b835fbda056c4f9 + url: https://conda.anaconda.org/conda-forge/win-64/vc14_runtime-14.40.33810-hcc2c482_22.conda + sha256: 4c669c65007f88a7cdd560192f7e6d5679d191ac71610db724e18b2410964d64 + md5: ce23a4b980ee0556a118ed96550ff3f3 depends: - ucrt >=10.0.20348.0 constrains: - - vs2015_runtime 14.40.33810.* *_21 - license: LicenseRef-ProprietaryMicrosoft + - vs2015_runtime 14.40.33810.* *_22 + license: LicenseRef-MicrosoftVisualCpp2015-2022Runtime license_family: Proprietary purls: [] - size: 751757 - timestamp: 1725984166774 + size: 750719 + timestamp: 1728401055788 - kind: conda name: vs2015_runtime version: 14.40.33810 - build: h3bf8584_21 - build_number: 21 + build: h3bf8584_22 + build_number: 22 subdir: win-64 - url: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_21.conda - sha256: 472410455c381e406ec8c1d3e0342b48ee23122ef7ffb22a09d9763ca5df4d20 - md5: b3f37db7b7ae1c22600fa26a63ed99b3 + url: https://conda.anaconda.org/conda-forge/win-64/vs2015_runtime-14.40.33810-h3bf8584_22.conda + sha256: 80aa9932203d65a96f817b8be4fafc176fb2b3fe6cf6899ede678b8f0317fbff + md5: 8c6b061d44cafdfc8e8c6eb5f100caf0 depends: - vc14_runtime >=14.40.33810 license: BSD-3-Clause license_family: BSD purls: [] - size: 17241 - timestamp: 1725984096440 + size: 17453 + timestamp: 1728400827536 - kind: pypi name: wcwidth version: 0.2.13 diff --git a/pyproject.toml b/pyproject.toml index 11d66a0..880d504 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -112,6 +112,9 @@ ignore = [ "E741", # ambiguous variable name ] +[project.entry-points.flopy4] +plot = "flopy4.singledispatch.plot_int" + [tool.pixi.project] channels = ["conda-forge"] platforms = ["win-64", "linux-64", "osx-64"] @@ -145,3 +148,4 @@ test = { cmd = "pytest -v -n auto" } [tool.pixi.feature.lint.tasks] lint = { cmd = "ruff check ." } + diff --git a/test/test_singledispatch.py b/test/test_singledispatch.py new file mode 100644 index 0000000..6e5337b --- /dev/null +++ b/test/test_singledispatch.py @@ -0,0 +1,62 @@ +import ast +import inspect +import subprocess +import sys +from importlib.metadata import entry_points + +import pytest + +from flopy4.singledispatch.plot import plot + + +def get_function_body(func): + source = inspect.getsource(func) + parsed = ast.parse(source) + for node in ast.walk(parsed): + if isinstance(node, ast.FunctionDef): + return ast.get_source_segment(source, node.body[0]) + raise ValueError("Function body not found") + + +def run_test_in_subprocess(test_func): + def wrapper(): + test_func_source = get_function_body(test_func) + test_code = f""" +import pytest +from importlib.metadata import entry_points +from flopy4.singledispatch.plot import plot + +{test_func_source} + +""" + result = subprocess.run( + [sys.executable, "-c", test_code], capture_output=True, text=True + ) + if result.returncode != 0: + print(result.stdout) + print(result.stderr) + assert result.returncode == 0, f"Test failed: {test_func.__name__}" + + return wrapper + + +@run_test_in_subprocess +def test_register_singledispatch_with_entrypoints(): + eps = entry_points(group="flopy4", name="plot") + for ep in eps: + ep.load() + + # should not throw an error, because plot_int was loaded via entry points + return_val = plot(5) + assert return_val == 5 + with pytest.raises(NotImplementedError): + plot("five") + + +@run_test_in_subprocess +def test_register_singledispatch_without_entrypoints(): + # should throw an error, because plot_int was not loaded via entry points + with pytest.raises(NotImplementedError): + plot(5) + with pytest.raises(NotImplementedError): + plot("five")