From 5b15a01edf7577204b0446c31c6a9a140ca5a294 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Robert?= Date: Wed, 15 Feb 2023 11:50:47 +0100 Subject: [PATCH] BLD: vendor a minimal version of munch --- amical/calibration.py | 4 +- amical/externals/munch/LICENSE | 21 ++ amical/externals/munch/__init__.py | 347 +++++++++++++++++++++++++++++ amical/mf_pipeline/ami_function.py | 5 +- amical/mf_pipeline/bispect.py | 3 +- amical/mf_pipeline/idl_function.py | 4 +- amical/oifits.py | 6 +- amical/tests/test_cli.py | 4 +- amical/tests/test_extraction.py | 16 +- amical/tests/test_io.py | 18 +- amical/tools.py | 4 +- pyproject.toml | 1 - 12 files changed, 395 insertions(+), 38 deletions(-) create mode 100644 amical/externals/munch/LICENSE create mode 100644 amical/externals/munch/__init__.py diff --git a/amical/calibration.py b/amical/calibration.py index 02a922e1..f9019416 100755 --- a/amical/calibration.py +++ b/amical/calibration.py @@ -12,6 +12,7 @@ import numpy as np from amical.dpfit import leastsqFit +from amical.externals.munch import munchify as dict2class from amical.tools import wtmn @@ -192,7 +193,6 @@ def average_calib_files(list_nrm, sig_thres=2, display=False): Threshold of the sigma clipping (default: 2-sigma around the median is used),\n """ from astropy.io import fits - from munch import munchify as dict2class nfiles = len(list_nrm) l_pa = np.zeros(nfiles) @@ -306,8 +306,6 @@ def calibrate( NRM data, inputs of this function). """ - from munch import munchify as dict2class - if type(res_c) is not list: res_c = [res_c] diff --git a/amical/externals/munch/LICENSE b/amical/externals/munch/LICENSE new file mode 100644 index 00000000..f02861ab --- /dev/null +++ b/amical/externals/munch/LICENSE @@ -0,0 +1,21 @@ +""" +Copyright (c) 2010 David Schoonover + +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. +""" diff --git a/amical/externals/munch/__init__.py b/amical/externals/munch/__init__.py new file mode 100644 index 00000000..7b2fa4db --- /dev/null +++ b/amical/externals/munch/__init__.py @@ -0,0 +1,347 @@ +# adapated from munch 2.5.0 +from collections.abc import Mapping + + +class Munch(dict): + """A dictionary that provides attribute-style access. + + >>> b = Munch() + >>> b.hello = 'world' + >>> b.hello + 'world' + >>> b['hello'] += "!" + >>> b.hello + 'world!' + >>> b.foo = Munch(lol=True) + >>> b.foo.lol + True + >>> b.foo is b['foo'] + True + + A Munch is a subclass of dict; it supports all the methods a dict does... + + >>> sorted(b.keys()) + ['foo', 'hello'] + + Including update()... + + >>> b.update({ 'ponies': 'are pretty!' }, hello=42) + >>> print (repr(b)) + Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42}) + + As well as iteration... + + >>> sorted([ (k,b[k]) for k in b ]) + [('foo', Munch({'lol': True})), ('hello', 42), ('ponies', 'are pretty!')] + + And "splats". + + >>> "The {knights} who say {ni}!".format(**Munch(knights='lolcats', ni='can haz')) + 'The lolcats who say can haz!' + + See unmunchify/Munch.toDict, munchify/Munch.fromDict for notes about conversion. + """ + + def __init__(self, *args, **kwargs): # pylint: disable=super-init-not-called + self.update(*args, **kwargs) + + # only called if k not found in normal places + def __getattr__(self, k): + """Gets key if it exists, otherwise throws AttributeError. + + nb. __getattr__ is only called if key is not found in normal places. + + >>> b = Munch(bar='baz', lol={}) + >>> b.foo + Traceback (most recent call last): + ... + AttributeError: foo + + >>> b.bar + 'baz' + >>> getattr(b, 'bar') + 'baz' + >>> b['bar'] + 'baz' + + >>> b.lol is b['lol'] + True + >>> b.lol is getattr(b, 'lol') + True + """ + try: + # Throws exception if not in prototype chain + return object.__getattribute__(self, k) + except AttributeError: + try: + return self[k] + except KeyError as exc: + raise AttributeError(k) from exc + + def __setattr__(self, k, v): + """Sets attribute k if it exists, otherwise sets key k. A KeyError + raised by set-item (only likely if you subclass Munch) will + propagate as an AttributeError instead. + + >>> b = Munch(foo='bar', this_is='useful when subclassing') + >>> hasattr(b.values, '__call__') + True + >>> b.values = 'uh oh' + >>> b.values + 'uh oh' + >>> b['values'] + Traceback (most recent call last): + ... + KeyError: 'values' + """ + try: + # Throws exception if not in prototype chain + object.__getattribute__(self, k) + except AttributeError: + try: + self[k] = v + except KeyError as exc: + raise AttributeError(k) from exc + else: + object.__setattr__(self, k, v) + + def __delattr__(self, k): + """Deletes attribute k if it exists, otherwise deletes key k. A KeyError + raised by deleting the key--such as when the key is missing--will + propagate as an AttributeError instead. + + >>> b = Munch(lol=42) + >>> del b.lol + >>> b.lol + Traceback (most recent call last): + ... + AttributeError: lol + """ + try: + # Throws exception if not in prototype chain + object.__getattribute__(self, k) + except AttributeError: + try: + del self[k] + except KeyError as exc: + raise AttributeError(k) from exc + else: + object.__delattr__(self, k) + + def toDict(self): + """Recursively converts a munch back into a dictionary. + + >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!') + >>> sorted(b.toDict().items()) + [('foo', {'lol': True}), ('hello', 42), ('ponies', 'are pretty!')] + + See unmunchify for more info. + """ + return unmunchify(self) + + @property + def __dict__(self): + return self.toDict() + + def __repr__(self): + """Invertible* string-form of a Munch. + + >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!') + >>> print (repr(b)) + Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42}) + >>> eval(repr(b)) + Munch({'ponies': 'are pretty!', 'foo': Munch({'lol': True}), 'hello': 42}) + + >>> with_spaces = Munch({1: 2, 'a b': 9, 'c': Munch({'simple': 5})}) + >>> print (repr(with_spaces)) + Munch({'a b': 9, 1: 2, 'c': Munch({'simple': 5})}) + >>> eval(repr(with_spaces)) + Munch({'a b': 9, 1: 2, 'c': Munch({'simple': 5})}) + + (*) Invertible so long as collection contents are each repr-invertible. + """ + return f"{self.__class__.__name__}({dict.__repr__(self)})" + + def __dir__(self): + return list(self.keys()) + + def __getstate__(self): + """Implement a serializable interface used for pickling. + + See https://docs.python.org/3.6/library/pickle.html. + """ + return {k: v for k, v in self.items()} + + def __setstate__(self, state): + """Implement a serializable interface used for pickling. + + See https://docs.python.org/3.6/library/pickle.html. + """ + self.clear() + self.update(state) + + __members__ = __dir__ # for python2.x compatibility + + @classmethod + def fromDict(cls, d): + """Recursively transforms a dictionary into a Munch via copy. + + >>> b = Munch.fromDict({'urmom': {'sez': {'what': 'what'}}}) + >>> b.urmom.sez.what + 'what' + + See munchify for more info. + """ + return munchify(d, cls) + + def copy(self): + return type(self).fromDict(self) + + def update(self, *args, **kwargs): + """ + Override built-in method to call custom __setitem__ method that may + be defined in subclasses. + """ + for k, v in dict(*args, **kwargs).items(): + self[k] = v + + def get(self, k, d=None): + """ + D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None. + """ + if k not in self: + return d + return self[k] + + def setdefault(self, k, d=None): + """ + D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D + """ + if k not in self: + self[k] = d + return self[k] + + +def munchify(x): + """Recursively transforms a dictionary into a Munch via copy. + + >>> b = munchify({'urmom': {'sez': {'what': 'what'}}}) + >>> b.urmom.sez.what + 'what' + + munchify can handle intermediary dicts, lists and tuples (as well as + their subclasses), but ymmv on custom datatypes. + + >>> b = munchify({ 'lol': ('cats', {'hah':'i win again'}), + ... 'hello': [{'french':'salut', 'german':'hallo'}] }) + >>> b.hello[0].french + 'salut' + >>> b.lol[1].hah + 'i win again' + + nb. As dicts are not hashable, they cannot be nested in sets/frozensets. + """ + # Munchify x, using `seen` to track object cycles + seen = dict() + + def munchify_cycles(obj): + # If we've already begun munchifying obj, just return the already-created munchified obj + try: + return seen[id(obj)] + except KeyError: + pass + + # Otherwise, first partly munchify obj (but without descending into any lists or dicts) and save that + seen[id(obj)] = partial = pre_munchify(obj) + # Then finish munchifying lists and dicts inside obj (reusing munchified obj if cycles are encountered) + return post_munchify(partial, obj) + + def pre_munchify(obj): + # Here we return a skeleton of munchified obj, which is enough to save for later (in case + # we need to break cycles) but it needs to filled out in post_munchify + if isinstance(obj, Mapping): + return Munch({}) + elif isinstance(obj, list): + return type(obj)() + elif isinstance(obj, tuple): + type_factory = getattr(obj, "_make", type(obj)) + return type_factory(munchify_cycles(item) for item in obj) + else: + return obj + + def post_munchify(partial, obj): + # Here we finish munchifying the parts of obj that were deferred by pre_munchify because they + # might be involved in a cycle + if isinstance(obj, Mapping): + partial.update((k, munchify_cycles(obj[k])) for k in obj.keys()) + elif isinstance(obj, list): + partial.extend(munchify_cycles(item) for item in obj) + elif isinstance(obj, tuple): + for item_partial, item in zip(partial, obj): + post_munchify(item_partial, item) + + return partial + + return munchify_cycles(x) + + +def unmunchify(x): + """Recursively converts a Munch into a dictionary. + + >>> b = Munch(foo=Munch(lol=True), hello=42, ponies='are pretty!') + >>> sorted(unmunchify(b).items()) + [('foo', {'lol': True}), ('hello', 42), ('ponies', 'are pretty!')] + + unmunchify will handle intermediary dicts, lists and tuples (as well as + their subclasses), but ymmv on custom datatypes. + + >>> b = Munch(foo=['bar', Munch(lol=True)], hello=42, + ... ponies=('are pretty!', Munch(lies='are trouble!'))) + >>> sorted(unmunchify(b).items()) #doctest: +NORMALIZE_WHITESPACE + [('foo', ['bar', {'lol': True}]), ('hello', 42), ('ponies', ('are pretty!', {'lies': 'are trouble!'}))] + + nb. As dicts are not hashable, they cannot be nested in sets/frozensets. + """ + + # Munchify x, using `seen` to track object cycles + seen = dict() + + def unmunchify_cycles(obj): + # If we've already begun unmunchifying obj, just return the already-created unmunchified obj + try: + return seen[id(obj)] + except KeyError: + pass + + # Otherwise, first partly unmunchify obj (but without descending into any lists or dicts) and save that + seen[id(obj)] = partial = pre_unmunchify(obj) + # Then finish unmunchifying lists and dicts inside obj (reusing unmunchified obj if cycles are encountered) + return post_unmunchify(partial, obj) + + def pre_unmunchify(obj): + # Here we return a skeleton of unmunchified obj, which is enough to save for later (in case + # we need to break cycles) but it needs to filled out in post_unmunchify + if isinstance(obj, Mapping): + return dict() + elif isinstance(obj, list): + return type(obj)() + elif isinstance(obj, tuple): + type_factory = getattr(obj, "_make", type(obj)) + return type_factory(unmunchify_cycles(item) for item in obj) + else: + return obj + + def post_unmunchify(partial, obj): + # Here we finish unmunchifying the parts of obj that were deferred by pre_unmunchify because they + # might be involved in a cycle + if isinstance(obj, Mapping): + partial.update((k, unmunchify_cycles(obj[k])) for k in obj.keys()) + elif isinstance(obj, list): + partial.extend(unmunchify_cycles(v) for v in obj) + elif isinstance(obj, tuple): + for value_partial, value in zip(partial, obj): + post_unmunchify(value_partial, value) + + return partial + + return unmunchify_cycles(x) diff --git a/amical/mf_pipeline/ami_function.py b/amical/mf_pipeline/ami_function.py index 3ea0f227..2b6a026f 100755 --- a/amical/mf_pipeline/ami_function.py +++ b/amical/mf_pipeline/ami_function.py @@ -20,6 +20,7 @@ from termcolor import cprint from amical.dpfit import leastsqFit +from amical.externals.munch import munchify as dict2class from amical.get_infos_obs import get_mask, get_pixel_size, get_wavelength from amical.mf_pipeline.idl_function import array_coords, dist from amical.tools import gauss_2d_asym, linear, norm_max, plot_circle @@ -183,8 +184,6 @@ def _peak_gauss_method( fw_splodge=0.7, hole_diam=0.8, ): - from munch import munchify as dict2class - mf = np.zeros([npix, npix]) n_holes = index_mask.n_holes @@ -365,7 +364,6 @@ def make_mf( Relative size of the splodge used to compute multiple triangle indices and the fwhm of the 'gauss' technique,\n """ - from munch import munchify as dict2class # Get detector, filter and mask informations # ------------------------------------------ @@ -631,7 +629,6 @@ def compute_index_mask(n_holes, verbose=False): Bispectrum covariance to bispectrum index. """ - from munch import munchify as dict2class n_baselines = int(n_holes * (n_holes - 1) / 2) n_bispect = int(n_holes * (n_holes - 1) * (n_holes - 2) / 6) diff --git a/amical/mf_pipeline/bispect.py b/amical/mf_pipeline/bispect.py index 911c0ca8..4fa20350 100644 --- a/amical/mf_pipeline/bispect.py +++ b/amical/mf_pipeline/bispect.py @@ -21,6 +21,7 @@ from termcolor import cprint from tqdm import tqdm +from amical.externals.munch import munchify as dict2class from amical.get_infos_obs import get_mask from amical.mf_pipeline.ami_function import ( bs_multi_triangle, @@ -349,7 +350,6 @@ def _check_input_infos(hdr, targetname=None, filtname=None, instrum=None, verbos input arguments. Return the infos class containing important informations of the input header (keys: target, seeing, instrument, ...) """ - from munch import munchify as dict2class target = hdr.get("OBJECT") filt = hdr.get("FILTER") @@ -1123,7 +1123,6 @@ def extract_bs( various quantities (see .mask.__dict__.keys()). """ from astropy.io import fits - from munch import munchify as dict2class if verbose: cprint("\n-- Starting extraction of observables --", "cyan") diff --git a/amical/mf_pipeline/idl_function.py b/amical/mf_pipeline/idl_function.py index 6b89f20e..cd4f987e 100755 --- a/amical/mf_pipeline/idl_function.py +++ b/amical/mf_pipeline/idl_function.py @@ -14,12 +14,12 @@ import numpy as np from termcolor import cprint +from amical.externals.munch import munchify as dict2class + def regress_noc(x, y, weights): """Python version of IDL regress_noc.""" - from munch import munchify as dict2class - sx = x.shape sy = y.shape nterm = sx[0] # # OF TERMS diff --git a/amical/oifits.py b/amical/oifits.py index 6a5b6216..5bbba121 100755 --- a/amical/oifits.py +++ b/amical/oifits.py @@ -15,6 +15,7 @@ import numpy as np from termcolor import cprint +from amical.externals.munch import munchify as dict2class from amical.tools import rad2mas list_color = ["#00a7b5", "#afd1de", "#055c63", "#ce0058", "#8a8d8f", "#f1b2dc"] @@ -69,8 +70,6 @@ def _format_staindex_t3(tab): def _apply_flag(dict_calibrated, unit="arcsec"): """Apply flag and convert to appropriete units.""" - from munch import munchify as dict2class - wl = dict_calibrated["OI_WAVELENGTH"]["EFF_WAVE"] uv_scale = { "m": 1, @@ -124,8 +123,6 @@ def wrap_raw(bs): Object that stores the raw observables in a format compatible with the output from amical.calibrate() and the input for `amical.save()`,\n """ - from munch import munchify as dict2class - u1 = bs.u[bs.mask.bs2bl_ix[0, :]] v1 = bs.v[bs.mask.bs2bl_ix[0, :]] u2 = bs.u[bs.mask.bs2bl_ix[1, :]] @@ -421,7 +418,6 @@ def load(filename, filtname=None): def loadc(filename): """Same as load but provide an easy usable output as a class format (output.v2, or output.cp).""" - from munch import munchify as dict2class dic = load(filename) res = {} diff --git a/amical/tests/test_cli.py b/amical/tests/test_cli.py index c5480853..b20da552 100644 --- a/amical/tests/test_cli.py +++ b/amical/tests/test_cli.py @@ -1,6 +1,5 @@ from pathlib import Path -import munch import numpy as np import pytest from astropy.io import fits @@ -8,6 +7,7 @@ from amical import load_bs_hdf5, loadc from amical._cli.main import main +from amical.externals.munch import Munch valid_commands = ["clean", "extract", "calibrate"] @@ -111,7 +111,7 @@ def test_extract(cli_datadir, tmp_path, monkeypatch): true_value_cp = 0.01 assert len(output_file) == 1 - assert isinstance(bs, munch.Munch) + assert isinstance(bs, Munch) assert len(bs_keys) == 13 assert bs.vis2[0] == pytest.approx(true_value_v2, 1e-1) assert bs.cp[0] == pytest.approx(true_value_cp, 1e-1) diff --git a/amical/tests/test_extraction.py b/amical/tests/test_extraction.py index 3abb5824..40683005 100644 --- a/amical/tests/test_extraction.py +++ b/amical/tests/test_extraction.py @@ -1,17 +1,17 @@ -import munch import pytest from astropy.io import fits +from amical.externals.munch import Munch, munchify from amical.mf_pipeline.bispect import _add_infos_header @pytest.fixture() def commentary_infos(): # Add hdr to infos placeholders for everything but hdr - mf = munch.Munch(pixelSize=1.0) + mf = Munch(pixelSize=1.0) # SimulatedData avoids requiring extra keys in infos - infos = munch.Munch(orig="SimulatedData", instrument="unknown") + infos = Munch(orig="SimulatedData", instrument="unknown") # Create a fits header with commentary card hdr = fits.Header() @@ -29,10 +29,10 @@ def test_add_infos_simulated(): hdr["TELESCOP"] = "FAKE-TEL" # SimulatedData avoids requiring extra keys in infos - infos = munch.Munch(orig="SimulatedData", instrument="unknown") + infos = Munch(orig="SimulatedData", instrument="unknown") # Add hdr to infos placeholders for everything but hdr - mf = munch.Munch(pixelSize=1.0) + mf = Munch(pixelSize=1.0) infos = _add_infos_header(infos, hdr, mf, 1.0, "afilename", "amaskname", 1) # Check that we kept required keys @@ -49,7 +49,7 @@ def test_add_infos_header_commentary(commentary_infos): # Make sure that _add_infos_header handles _HeaderCommentaryCards from astropy # Convert everything to munch object - munch.munchify(commentary_infos) + munchify(commentary_infos) def test_commentary_infos_keep(commentary_infos): @@ -58,10 +58,10 @@ def test_commentary_infos_keep(commentary_infos): def test_no_commentary_warning_astropy_version(): # Add hdr to infos placeholders for everything but hdr - mf = munch.Munch(pixelSize=1.0) + mf = Munch(pixelSize=1.0) # SimulatedData avoids requiring extra keys in infos - infos = munch.Munch(orig="SimulatedData", instrument="unknown") + infos = Munch(orig="SimulatedData", instrument="unknown") # Create a fits header with commentary card hdr = fits.Header() diff --git a/amical/tests/test_io.py b/amical/tests/test_io.py index a5f3fd9c..8b402859 100644 --- a/amical/tests/test_io.py +++ b/amical/tests/test_io.py @@ -1,6 +1,5 @@ import re -import munch import numpy as np import pytest from astropy.io import fits @@ -8,6 +7,7 @@ import amical from amical import load, loadc +from amical.externals.munch import Munch from amical.get_infos_obs import get_pixel_size # import numpy as np @@ -81,7 +81,7 @@ def test_extract(peakmethod, global_datadir): peakmethod=peakmethod, ) bs_keys = list(bs.keys()) - assert isinstance(bs, munch.Munch) + assert isinstance(bs, Munch) assert len(bs_keys) == 13 @@ -105,7 +105,7 @@ def test_extract_multitri(global_datadir, tmp_path): peakmethod="fft", ) bs_keys = list(bs.keys()) - assert isinstance(bs, munch.Munch) + assert isinstance(bs, Munch) assert len(bs_keys) == 13 @@ -143,7 +143,7 @@ def test_cal_atmcorr(global_datadir): ) cal = amical.calibrate(bs, bs, apply_atmcorr=True) - assert isinstance(cal, munch.Munch) + assert isinstance(cal, Munch) def test_cal_phscorr(global_datadir): @@ -161,11 +161,11 @@ def test_cal_phscorr(global_datadir): peakmethod="fft", ) cal = amical.calibrate(bs, bs, apply_atmcorr=True) - assert isinstance(cal, munch.Munch) + assert isinstance(cal, Munch) def test_calibration(cal): - assert isinstance(cal, munch.Munch) + assert isinstance(cal, Munch) def test_show(cal): @@ -275,7 +275,7 @@ def test_save_origin(cal, tmpdir): def test_loadc_file(example_oifits): s = loadc(example_oifits) - assert isinstance(s, munch.Munch) + assert isinstance(s, Munch) @pytest.mark.parametrize("ins", ["NIRISS", "SPHERE", "VAMPIRES"]) @@ -301,7 +301,7 @@ def test_quiet_mode(global_datadir, capsys): ) captured = capsys.readouterr() bs_keys = list(bs.keys()) - assert isinstance(bs, munch.Munch) + assert isinstance(bs, Munch) assert len(bs_keys) == 13 assert captured.out == "" assert captured.err == "" @@ -358,7 +358,7 @@ def test_extract_ifs(global_datadir, ifs_clean_param): fw_splodge=0.7, display=False, ) - assert isinstance(bs, munch.Munch) + assert isinstance(bs, Munch) assert len(bs) == 13 diff --git a/amical/tools.py b/amical/tools.py index 91418704..fee59881 100644 --- a/amical/tools.py +++ b/amical/tools.py @@ -15,6 +15,8 @@ import numpy as np from termcolor import cprint +from amical.externals.munch import munchify as dict2class + def linear(x, param): """Linear model used in dpfit""" @@ -595,7 +597,6 @@ def check_seeing_cond(list_nrm): # pragma: no cover """ from astropy.io import fits - from munch import munchify as dict2class l_seeing, l_vis2, l_cp, l_pa, l_mjd = [], [], [], [], [] @@ -733,7 +734,6 @@ def load_bs_hdf5(filename): format as `amical.extract_bs()` """ import h5py - from munch import munchify as dict2class dict_bs = {"matrix": {}, "infos": {"hdr": {}}, "mask": {}} with h5py.File(filename, "r") as hf2: diff --git a/pyproject.toml b/pyproject.toml index aa56f0fc..c290e7bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ dependencies = [ "emcee", "h5py", "matplotlib", - "munch", "numpy", "pypdf>=3.2.0", "scipy",