diff --git a/order/category.py b/order/category.py index 8a24330..adec473 100644 --- a/order/category.py +++ b/order/category.py @@ -8,7 +8,7 @@ __all__ = ["Channel", "Category"] -from order.unique import UniqueObject, unique_tree +from order.unique import UniqueObject, UniqueObjectIndex, unique_tree from order.mixins import CopyMixin, AuxDataMixin, TagMixin, SelectionMixin, LabelMixin from order.util import to_root_latex @@ -37,8 +37,15 @@ class Category(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, SelectionMixin, **Copy behavior** + ``copy()`` + The :py:attr:`channel` attribute is carried over as a reference, all remaining attributes are - copied. Note that the copied dataset is also registered in the channel. + copied. Note that the copied category is also registered in the channel. + + ``copy_shallow()`` + + All attributs are copied except for the :py:attr:`channel` and (child) :py:attr:`categories` + which are set to default values instead. **Example** @@ -137,7 +144,18 @@ class Category(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, SelectionMixin, # attributes for copying copy_specs = ( - [{"attr": "_channel", "ref": True}] + + [ + { + "attr": "_channel", + "ref": True, + "skip_shallow": True, + }, + { + "attr": "_categories", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Category)), + }, + ] + UniqueObject.copy_specs + AuxDataMixin.copy_specs + TagMixin.copy_specs + @@ -243,8 +261,15 @@ class Channel(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, LabelMixin): **Copy behavior** + ``copy()`` + All attributes are copied. + ``copy_shallow()`` + + All attributs are copied except for child :py:attr:`categories` which is set to a default value + instead. + **Example** .. code-block:: python @@ -286,6 +311,13 @@ class Channel(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, LabelMixin): # attributes for copying copy_specs = ( + [ + { + "attr": "_categories", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Category)), + }, + ] + UniqueObject.copy_specs + AuxDataMixin.copy_specs + TagMixin.copy_specs + diff --git a/order/config.py b/order/config.py index 60f1e8e..093a0df 100644 --- a/order/config.py +++ b/order/config.py @@ -9,7 +9,7 @@ __all__ = ["Campaign", "Config"] -from order.unique import UniqueObject, unique_tree +from order.unique import UniqueObject, UniqueObjectIndex, unique_tree from order.mixins import CopyMixin, AuxDataMixin, TagMixin from order.shift import Shift from order.dataset import Dataset @@ -35,8 +35,15 @@ class Campaign(UniqueObject, CopyMixin, AuxDataMixin, TagMixin): **Copy behavior** + ``copy()`` + All attributes are copied. + ``copy_shallow()`` + + All attributs are copied except for contained :py:attr:`datasets` which are set to a default + value instead. + **Example** .. code-block:: python @@ -75,6 +82,13 @@ class Campaign(UniqueObject, CopyMixin, AuxDataMixin, TagMixin): cls_name_plural = "campaigns" copy_specs = ( + [ + { + "attr": "_datasets", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Dataset)), + }, + ] + UniqueObject.copy_specs + AuxDataMixin.copy_specs + TagMixin.copy_specs @@ -170,9 +184,17 @@ class Config(UniqueObject, CopyMixin, AuxDataMixin, TagMixin): **Copy behavior** + ``copy()`` + The :py:attr:`campaign` and :py:attr:`analysis` attributes are carried over as references, all remaining attributes are copied. Note that the copied config is also registered in the analysis. + ``copy_shallow()`` + + All attributs are copied except for the :py:attr:`analysis` and :py:attr:`campaign`, as well as + contained :py:attr:`datasets`, :py:attr:`processes`, :py:attr:`channels`, :py:attr:`variables` + and :py:attr:`shifts` which are set to default values instead. + **Example** .. code-block:: python @@ -228,8 +250,46 @@ class Config(UniqueObject, CopyMixin, AuxDataMixin, TagMixin): copy_specs = ( [ - {"attr": "_campaign", "ref": True}, - {"attr": "_analysis", "ref": True}, + { + "attr": "_campaign", + "ref": True, + "skip_shallow": True, + }, + { + "attr": "_analysis", + "ref": True, + "skip_shallow": True, + }, + { + "attr": "_datasets", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Dataset)), + }, + { + "attr": "_processes", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Process)), + }, + { + "attr": "_channels", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Channel)), + }, + { + "attr": "_categories", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Category)), + }, + { + "attr": "_variables", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Variable)), + }, + { + "attr": "_shifts", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Shift)), + }, ] + UniqueObject.copy_specs + AuxDataMixin.copy_specs + diff --git a/order/dataset.py b/order/dataset.py index b470869..e68c9e5 100644 --- a/order/dataset.py +++ b/order/dataset.py @@ -10,7 +10,7 @@ import six -from order.unique import UniqueObject, unique_tree +from order.unique import UniqueObject, UniqueObjectIndex, unique_tree from order.mixins import CopyMixin, AuxDataMixin, TagMixin, DataSourceMixin, LabelMixin from order.process import Process from order.shift import Shift @@ -48,9 +48,16 @@ class Dataset(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, DataSourceMixin, **Copy behavior** + ``copy()`` + The :py:attr:`campaign` attribute is carried over as a reference, all remaining attributes are copied. Note that the copied dataset is also registered in the campaign. + ``copy_shallow()`` + + All attributs are copied except for the :py:attr:`campaign` and containd :py:attr:`processes` + which are set to default values instead. + **Example** .. code-block:: python @@ -145,7 +152,18 @@ class Dataset(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, DataSourceMixin, # attributes for copying copy_specs = ( - [{"attr": "_campaign", "ref": True}] + + [ + { + "attr": "_campaign", + "ref": True, + "skip_shallow": True, + }, + { + "attr": "_processes", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Process)), + }, + ] + UniqueObject.copy_specs + AuxDataMixin.copy_specs + TagMixin.copy_specs + diff --git a/order/mixins.py b/order/mixins.py index 37b8b72..0a7d51a 100644 --- a/order/mixins.py +++ b/order/mixins.py @@ -37,6 +37,10 @@ class CopySpec(object): this is done be temporarily setting the attribute to a *ref_placeholder* value, performing the deep copy, and then (re)setting the attribute to the original object. + When *skip* (*skip_shallow*) is *True*, the attribute is not copied when the source objects is + copied through :py:meth:`CopyMixin.copy` (:py:meth:`CopyMixin.copy_shallow`). When skipped, + the attribute of the copied object will be *skip_value*. + **Members** .. py:attribute:: dst @@ -52,12 +56,28 @@ class CopySpec(object): .. py:attribute:: ref type: bool - Whether or not the attribute should be passed as a reference instead of copying. + Whether or not the attribute should be passed as a reference instead of being copied. .. py:attribute:: ref_placeholder type: any - Placeholder value that is used during the copying process. + Placeholder value for attributes carried over as a reference that is used during the copying + process. + + .. py:attribute:: skip + type: bool + + Whether or not the attribute is skipped when copied with :py:meth:`CopyMixin.copy`. + + .. py:attribute:: skip_shallow + type: bool + + Whether or not the attribute is skipped when copyied with :py:meth:`CopyMixin.copy_shallow`. + + .. py:attribute:: skip_value + type: any + + Value to be used for attributes that are skipped in the copy process. """ @classmethod @@ -78,13 +98,30 @@ def new(cls, obj): msg = "cannot create {} from dict, a 'dst' or 'attr' field is required, got '{}'" raise ValueError(msg.format(cls.__name__, obj)) kwargs["src"] = obj.get("src", obj.get("attr")) - kwargs["ref"] = obj.get("ref", False) - kwargs["ref_placeholder"] = obj.get("ref_placeholder", None) + if "ref" in obj: + kwargs["ref"] = obj.get("ref", False) + if "ref_placeholder" in obj: + kwargs["ref_placeholder"] = obj.get("ref_placeholder", None) + if "skip" in obj: + kwargs["skip"] = obj.get("skip", False) + if "skip_shallow" in obj: + kwargs["skip_shallow"] = obj.get("skip_shallow", False) + if "skip_value" in obj: + kwargs["skip_value"] = obj.get("skip_value") return cls(**kwargs) raise TypeError("cannot create {} from object '{}'".format(cls.__name__, obj)) - def __init__(self, dst, src=None, ref=False, ref_placeholder=None): + def __init__( + self, + dst, + src=None, + ref=False, + ref_placeholder=None, + skip=False, + skip_shallow=False, + skip_value=None, + ): super(CopySpec, self).__init__() # store attributes @@ -92,6 +129,9 @@ def __init__(self, dst, src=None, ref=False, ref_placeholder=None): self.src = src or dst self.ref = ref self.ref_placeholder = ref_placeholder + self.skip = skip + self.skip_shallow = skip_shallow + self.skip_value = skip_value def __eq__(self, spec): """ @@ -123,7 +163,8 @@ class CopyMixin(object): .. note:: At the moment, custom specs are only required for attributes that should not be copied but - carried over to the copied instance as a reference. + either carried over to the copied instance as a reference, or skipped in case only a shallow + copy is requested via :py:meth:`copy_shallow`. **Example** @@ -137,17 +178,21 @@ class MyClass(od.CopyMixin): copy_specs = [ {"attr": "obj", "ref": True}, + {"attr": "complex_obj", "ref": True, "skip_shallow": True}, ] - def __init__(self, name, obj): + def __init__(self, name, obj, complex_obj=None): super(MyClass, self).__init__() self.name = name self.obj = obj + self.complex_obj = complex_obj - a = MyClass("foo", some_object) + a = MyClass("foo", some_object, "some_other_complex_object") a.name # -> "foo" + # normal copy + b = a.copy() b.name # -> "foo" @@ -155,6 +200,21 @@ def __init__(self, name, obj): b.obj is a.obj # -> True + b.complex_obj is a.complext_obj + # -> True + + # shallow copy (skipping certain attributes) + + c = a.copy_shallow() + c.name + # -> "foo" + + c.obj is a.obj + # -> True + + c.complex_obj is None # note the None here + # -> True + **Members** .. py:classattribute:: copy_specs @@ -163,46 +223,76 @@ def __init__(self, name, obj): List of copy specifications per attribute. """ - copy_spec = [] + class Deferred(object): - def copy(self, *args, **kwargs): - r"""copy(*args, **kwargs, _specs=None, _skip=None) - Creates a copy of this instance and returns it. All *args* and *kwargs* are converted to - named arguments (based on the *init* signature) and set as attributes of the created copy. - Additional specifications per attribute are taken from :py:attr:`copy_specs` or *_specs* if - set. *_skip* can be a sequence of source attribute names that should be skipped. - """ - # extract the copy configuration from kwargs - specs = kwargs.pop("_specs", None) or self.copy_specs - skip = kwargs.pop("_skip", None) or [] + def __init__(self, func): + self.func = func - # unite args and kwargs - kwargs.update(args_to_kwargs(self.__class__.__init__ if six.PY2 else self.__class__, args)) + def __call__(self, *args, **kwargs): + return self.func(*args, **kwargs) + copy_spec = [] + + @classmethod + def _create_specs(cls, specs): # ensure that specs contain CopySpec objects - # also remove duplicates, prioritize last occurances + # also remove duplicates, priotize last occurances _specs = [] + for spec in specs[::-1]: - if not isinstance(spec, CopySpec): - spec = CopySpec.new(spec) + spec = CopySpec.new(spec.__dict__ if isinstance(spec, CopySpec) else spec) if spec not in _specs: _specs.append(spec) - specs = _specs[::-1] - # maybe skip some specs, identified by the source attribute - specs = list(filter((lambda spec: spec.src not in skip), specs)) + return _specs[::-1] - # attributes that are not copied but carried over as a reference should be set to a - # placeholder first and reset later on + def copy(self, *args, **kwargs): + r"""copy(*args, **kwargs, _specs=None, _skip=None) + Creates a copy of this instance and returns it. All *args* and *kwargs* are converted to + named arguments (based on the *init* signature) and set as attributes of the created copy. + Additional specifications per attribute are taken from :py:attr:`copy_specs` or *_specs* if + set. *_skip* can be a sequence of source attribute names that should be skipped. + """ + # get CopySpec objects + specs = self._create_specs(kwargs.pop("_specs", None) or self.copy_specs) + + # apply additional skips + for src in (kwargs.pop("_skip", None) or []): + for spec in specs: + if spec.src == src: + spec.skip = True + break + + # attributes that are skipped or carried over as a reference should be set to a placeholder + # first and reset later on + skips = {} refs = {} for spec in specs: - if spec.ref: + if spec.skip: + skip_value = ( + spec.skip_value() + if isinstance(spec.skip_value, self.Deferred) else + spec.skip_value + ) + skips[spec] = getattr(self, spec.src) + setattr(self, spec.src, skip_value) + elif spec.ref: + ref_placeholder = ( + spec.ref_placeholder() + if isinstance(spec.ref_placeholder, self.Deferred) else + spec.ref_placeholder + ) refs[spec] = getattr(self, spec.src) - setattr(self, spec.src, spec.ref_placeholder) + setattr(self, spec.src, ref_placeholder) # perform the deep copy operation inst = copy.deepcopy(self) + # reset skipped attributes + for spec, obj in skips.items(): + # reset the old value of this instance + setattr(self, spec.src, obj) + # reset references for spec, obj in refs.items(): # reset the old value of this instance @@ -210,12 +300,31 @@ def copy(self, *args, **kwargs): # add a reference to the copied instance setattr(inst, spec.dst, obj) + # unite args and kwargs + kwargs.update(args_to_kwargs(self.__class__.__init__ if six.PY2 else self.__class__, args)) + # set additional attributes for attr, obj in kwargs.items(): setattr(inst, attr, obj) return inst + def copy_shallow(self, *args, **kwargs): + r"""copy_shallow(*args, **kwargs, _specs=None, _skip=None) + Just like :py:meth:`copy`, creates a copy of this instance and returns it, however with all + attributes that were declared as *skip_shallow* skipped (see :py:class:`CopySpec`). + """ + # get CopySpec objects + specs = self._create_specs(kwargs.pop("_specs", None) or self.copy_specs) + + # use skip_shallow flags + for spec in specs: + spec.skip |= spec.skip_shallow + + # use copy implementation + kwargs["_specs"] = specs + return self.copy(*args, **kwargs) + class AuxDataMixin(object): """ diff --git a/order/process.py b/order/process.py index ec91a8a..6ed1cb3 100644 --- a/order/process.py +++ b/order/process.py @@ -13,7 +13,7 @@ import six from scinum import Number -from order.unique import UniqueObject, unique_tree +from order.unique import UniqueObject, UniqueObjectIndex, unique_tree from order.mixins import CopyMixin, AuxDataMixin, TagMixin, DataSourceMixin, LabelMixin, ColorMixin from order.util import typed @@ -48,8 +48,15 @@ class Process( **Copy behavior** + ``copy()`` + All attributes are copied. + ``copy_shallow()`` + + All attributes except for (child) :py:attr:`processes` and :py:attr:`parent_processes` are + copied. + **Example** .. code-block:: python @@ -104,6 +111,18 @@ class Process( # attributes for copying copy_specs = ( + [ + { + "attr": "_processes", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Process)), + }, + { + "attr": "_parent_processes", + "skip_shallow": True, + "skip_value": CopyMixin.Deferred(lambda: UniqueObjectIndex(cls=Process)), + }, + ] + UniqueObject.copy_specs + AuxDataMixin.copy_specs + TagMixin.copy_specs + diff --git a/order/shift.py b/order/shift.py index 9fa4f89..fd2effe 100644 --- a/order/shift.py +++ b/order/shift.py @@ -33,8 +33,14 @@ class Shift(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, LabelMixin): **Copy behavior** + ``copy()`` + All attributes are copied. + ``copy_shallow()`` + + No difference with respect to ``copy()``, all attributes are copied. + **Example** .. code-block:: python diff --git a/order/variable.py b/order/variable.py index 55468c7..175ce03 100644 --- a/order/variable.py +++ b/order/variable.py @@ -43,8 +43,14 @@ class Variable(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, SelectionMixin): **Copy behavior** + ``copy()`` + All attributes are copied. + ``copy_shallow()`` + + No difference with respect to ``copy()``, all attributes are copied. + **Example** .. code-block:: python @@ -184,7 +190,7 @@ class Variable(UniqueObject, CopyMixin, AuxDataMixin, TagMixin, SelectionMixin): type: boolean Whether or not the x-axis is partitioned by discrete values (i.e, an integer axis). There is - not constraint on the :py:attribute:`binning` setting, but it should be set accordingly. + not constraint on the :py:attr:`binning` setting, but it should be set accordingly. .. py:attribute:: y_discrete type: boolean diff --git a/tests/test_category.py b/tests/test_category.py index d639e34..cbc968b 100644 --- a/tests/test_category.py +++ b/tests/test_category.py @@ -93,6 +93,7 @@ def test_copy(self): c.add_category("eq4j_eq2b") self.assertEqual(len(SL.categories), 1) + self.assertEqual(len(c.categories), 1) c2 = c.copy(name="SL2", id="+") @@ -100,3 +101,18 @@ def test_copy(self): self.assertEqual(len(c.categories), 1) self.assertEqual(len(c2.categories), 1) self.assertEqual(c2.channel, c.channel) + + def test_copy_shallow(self): + SL = Channel("SL", 1) + c = Category("eq4j", channel=SL) + c.add_category("eq4j_eq2b") + + self.assertEqual(len(SL.categories), 1) + self.assertEqual(len(c.categories), 1) + + c2 = c.copy_shallow() + + self.assertEqual(len(SL.categories), 1) + self.assertEqual(len(c.categories), 1) + self.assertEqual(len(c2.categories), 0) + self.assertIsNone(c2.channel) diff --git a/tests/test_config.py b/tests/test_config.py index f72e1b8..32637c2 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -68,6 +68,19 @@ def test_copy(self): a2.datasets.get_first().processes.get_first(), ) + def test_copy_shallow(self): + a = Campaign("2017A", 1) + d = Dataset("ttH", 1, campaign=a) + p = d.add_process("ttH_bb", 2) + + a2 = a.copy_shallow() + + self.assertEqual(a2.name, "2017A") + self.assertEqual(a2.id, 1) + self.assertEqual(len(a.datasets), 1) + self.assertEqual(len(a2.datasets), 0) + self.assertIs(a.datasets.n.ttH.processes.n.ttH_bb, p) + class ConfigTest(unittest.TestCase): @@ -94,3 +107,14 @@ def test_copy(self): self.assertEqual(c2.name, "2017B") self.assertEqual(c2.id, 2) self.assertEqual(c2.campaign, c.campaign) + + def test_copy_shallow(self): + a = Campaign("2017A", 1) + c = Config(a) + + c2 = c.copy_shallow() + + self.assertEqual(c2.name, "2017A") + self.assertEqual(c2.id, 1) + self.assertIsNone(c2.analysis) + self.assertIsNone(c2.campaign) diff --git a/tests/test_dataset.py b/tests/test_dataset.py index 9a60f49..af06214 100644 --- a/tests/test_dataset.py +++ b/tests/test_dataset.py @@ -114,6 +114,26 @@ def test_copy(self): self.assertEqual(len(d.campaign.datasets), 2) self.assertTrue(d2 in d.campaign.datasets) + def test_copy_shallow(self): + c = Campaign("2017B", 2) + d = Dataset( + name="ttH", + id=1, + campaign=c, + keys=["/ttHTobb_M125.../.../..."], + n_files=123, + n_events=456789, + ) + d.add_process("ttH", 1) + d2 = d.copy_shallow() + + self.assertEqual(d2.name, "ttH") + self.assertEqual(d2.id, 1) + self.assertEqual(len(d.processes), 1) + self.assertEqual(len(d2.processes), 0) + self.assertIsNone(d2.campaign) + self.assertEqual(len(d.campaign.datasets), 1) + class DatasetInfoTest(unittest.TestCase): diff --git a/tests/test_process.py b/tests/test_process.py index 0e4b403..082de8e 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -52,10 +52,13 @@ def test_attributes(self): def test_copy(self): p = Process("ttVV", 7, xsecs={13: 5}, color=(0.3, 0.4, 0.5), is_data=False, aux={1: 2}) p.add_process("ttVV_dl", 8) + p.add_parent_process("ttX", 6) self.assertEqual(len(p.processes), 1) + self.assertEqual(len(p.parent_processes), 1) p2 = p.copy(name="ttVVV", id=9, aux={3: 4}) self.assertEqual(len(p2.processes), 1) + self.assertEqual(len(p2.parent_processes), 1) self.assertEqual(p2.name, "ttVVV") self.assertEqual(p2.id, 9) @@ -66,6 +69,27 @@ def test_copy(self): self.assertFalse(p2.has_aux(1)) self.assertEqual(p.get_process(8).get_parent_process(7), p) self.assertEqual(p2.get_process(8).get_parent_process(9), p2) + self.assertEqual(p.get_parent_process(6).get_process(7), p) + self.assertEqual(p2.get_parent_process(6).get_process(9), p2) + + def test_copy_shallow(self): + p = Process("ttVV", 7, xsecs={13: 5}, color=(0.3, 0.4, 0.5), is_data=False, aux={1: 2}) + p.add_process("ttVV_dl", 8) + p.add_parent_process("ttX", 6) + self.assertEqual(len(p.processes), 1) + self.assertEqual(len(p.parent_processes), 1) + + p2 = p.copy_shallow(name="ttVVV", id=9, aux={3: 4}) + self.assertEqual(len(p2.processes), 0) + self.assertEqual(len(p2.parent_processes), 0) + + self.assertEqual(p2.name, "ttVVV") + self.assertEqual(p2.id, 9) + self.assertEqual(p2.get_xsec(13), 5) + self.assertEqual(p2.color, p.color) + self.assertEqual(list(p2.aux.keys())[0], 3) + self.assertTrue(p2.has_aux(3)) + self.assertFalse(p2.has_aux(1)) def test_parent_processes(self): c = Process("child", 10) diff --git a/tests/test_shift.py b/tests/test_shift.py index 8cbefeb..11a5e14 100644 --- a/tests/test_shift.py +++ b/tests/test_shift.py @@ -81,3 +81,19 @@ def test_copy(self): self.assertEqual(s3.source, s.source) self.assertEqual(s3.type, s.type) self.assertEqual(s3.label, s.label) + + def test_copy_shallow(self): + s = Shift("scale_down", 5, type=Shift.SHAPE) + s2 = s.copy_shallow(name="scale_up", id=6, label_short="sup") + + self.assertEqual(s2.name, "scale_up") + self.assertEqual(s2.type, Shift.SHAPE) + self.assertEqual(s2.label, s2.name) + self.assertEqual(s2.label_short, "sup") + + s3 = s.copy() + self.assertEqual(s3.name, s.name) + self.assertEqual(s3.direction, s.direction) + self.assertEqual(s3.source, s.source) + self.assertEqual(s3.type, s.type) + self.assertEqual(s3.label, s.label) diff --git a/tests/test_variable.py b/tests/test_variable.py index aea06a7..d1c6ebf 100644 --- a/tests/test_variable.py +++ b/tests/test_variable.py @@ -192,6 +192,23 @@ def test_copy(self): self.assertEqual(v3.expression, v.expression) self.assertEqual(v3.selection, v.selection) + def test_copy_shallow(self): + v = self.make_var("copy_var") + v2 = v.copy_shallow( + name="otherVar", + id=Variable.AUTO_ID, + expression="otherExpression", + ) + + self.assertEqual(v2.name, "otherVar") + self.assertEqual(v2.expression, "otherExpression") + self.assertEqual(v2.selection, "myBranchC > 0") + + v3 = v.copy() + self.assertEqual(v3.name, v.name) + self.assertEqual(v3.expression, v.expression) + self.assertEqual(v3.selection, v.selection) + def test_mpl_data(self): v = self.make_var( name="mpl_hist",