Repository: idaholab/MontePy

Issue:
Cannot nullify Cell.universe
## Bug Description

A cell's universe attribute cannot be set to `None` or deleted, essentially forcing `Cell.universe` to always be set to something once it has been set.

A new `Cell` is instantiated with a null universe attribute.


## To Reproduce

<!-- A short code snippet of what you have ran. Please change or remove any specific values or anything that can't be public. For example: --> 

``` python
>>> import montepy
>>> c = montepy.Cell("1 0 +1 imp:p=0")
>>> c.universe is None
True
>>> c.universe = None
TypeError
>>> del c.universe
AttributeError
```

## Error Message 

First error:

<details closed> 

``` python
>>> c.universe = None
Traceback (most recent call last):
  File "<python-input-2>", line 1, in <module>
    c.universe = None
    ^^^^^^^^^^
  File "./montepy/mcnp_object.py", line 49, in wrapped
    add_line_number_to_exception(e, self)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "./montepy/exceptions.py", line 235, in add_line_number_to_exception
    raise error
  File "./montepy/mcnp_object.py", line 41, in wrapped
    return func(*args, **kwargs)
  File "./montepy/mcnp_object.py", line 150, in __setattr__
    descriptor.__set__(self, value)
    ~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^
  File "./montepy/mcnp_object.py", line 49, in wrapped
    add_line_number_to_exception(e, self)
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^
  File "./montepy/exceptions.py", line 259, in add_line_number_to_exception
    raise error.with_traceback(trace)
  File "./montepy/mcnp_object.py", line 41, in wrapped
    return func(*args, **kwargs)
  File "./montepy/cell.py", line 258, in universe
    raise TypeError("universe must be set to a Universe")
TypeError: universe must be set to a Universe

Error came from CELL: 1, mat: 0, DENS: None from an unknown file.
```
</details>

Second error:

<details closed>

``` python
>>> del c.universe
Traceback (most recent call last):
  File "<python-input-5>", line 1, in <module>
    del c.universe
        ^^^^^^^^^^
AttributeError: property 'universe' of 'Cell' object has no deleter
```
</details>

## Version

 - Version 1.3.0

## Interface

Type: Function
Name: universe
Path: montepy/cell.py
Input: self
Output: Universe or None
Description: Returns the current universe assigned to this cell. Use this to access or reset a cell's universe; setting it to None or deleting it resets the assignment to the default (universe 0) if the cell is within a problem, or clears it entirely if not.

Discussion:

Fix (patch):
diff --git a/doc/source/changelog.rst b/doc/source/changelog.rst
index e120f8dea..c1a7548fb 100644
--- a/doc/source/changelog.rst
+++ b/doc/source/changelog.rst
@@ -25,6 +25,7 @@ MontePy Changelog
 
 **Feature Added**
 
+* ``Cell.universe`` can now be set to ``None`` (or deleted via ``del cell.universe``) to reset the universe assignment back to the default (:issue:`902`).
 * Added ``extend_renumber`` to ``NumberedObjectCollection`` with related test cases (:issue:`881`).
 * Made :class:`montepy.data_inputs.importance.Importance` more ``dict``-like with ``keys``, ``values``, and ``items`` functions (:pull:`921`).
 
diff --git a/montepy/cell.py b/montepy/cell.py
index 18b4f9f1b..2f656aeb3 100644
--- a/montepy/cell.py
+++ b/montepy/cell.py
@@ -255,10 +255,22 @@ def universe(self):
 
     @universe.setter
     def universe(self, value):
+        if value is None:
+            if self._problem:
+                if 0 not in self._problem.universes.numbers:
+                    self._problem.universes.append(Universe(0))
+                value = self._problem.universes[0]
+            else:
+                self._universe._universe = None
+                return
         if not isinstance(value, Universe):
             raise TypeError("universe must be set to a Universe")
         self._universe.universe = value
 
+    @universe.deleter
+    def universe(self):
+        self.universe = None
+
     @property
     def fill(self):
         """the Fill object representing how this cell is filled.


Test changes:
diff --git a/tests/test_universe.py b/tests/test_universe.py
index d70119180..eee444aa2 100644
--- a/tests/test_universe.py
+++ b/tests/test_universe.py
@@ -564,3 +564,34 @@ def verify_export(self, fill):
                 assert (old_val == new_val).all()
             else:
                 assert old_val == new_val
+
+
+def test_cell_universe_nullify_no_problem():
+    import montepy
+
+    c = montepy.Cell()
+    u = montepy.Universe(5)
+    c.universe = u
+    assert c.universe.number == 5
+    c.universe = None
+    assert c.universe is None
+
+
+def test_cell_universe_delete_no_problem():
+    import montepy
+
+    c = montepy.Cell()
+    u = montepy.Universe(5)
+    c.universe = u
+    assert c.universe.number == 5
+    del c.universe
+    assert c.universe is None
+
+
+def test_cell_universe_setter_type_error():
+    import montepy
+    import pytest
+
+    c = montepy.Cell()
+    with pytest.raises(TypeError):
+        c.universe = 5
diff --git a/tests/test_universe_integration.py b/tests/test_universe_integration.py
index a662bb67a..d100d8a33 100644
--- a/tests/test_universe_integration.py
+++ b/tests/test_universe_integration.py
@@ -46,6 +46,24 @@ def test_universe_setter(cells):
         cell_verify(basic_cell)
 
 
+def test_universe_nullify(cells):
+    """Cell.universe can be set to None (or deleted) to reset to the default."""
+    for basic_cell in cells:
+        uni = Universe(5)
+        basic_cell.universe = uni
+        assert basic_cell.universe is not None
+
+        # Setting to None should work
+        basic_cell.universe = None
+        assert basic_cell.universe is None
+
+        # Setting back, then using del
+        basic_cell.universe = uni
+        assert basic_cell.universe is not None
+        del basic_cell.universe
+        assert basic_cell.universe is None
+
+
 def test_fill_setter(cells):
     for basic_cell in cells:
         uni = Universe(5)
@@ -147,3 +165,48 @@ def test_fill_long_mcnp_str_wrap(cells):
             assert len(obj.mcnp_str().split("\n")) > 1
             montepy.MCNP_VERSION = old_vers
             prob.mcnp_version = montepy.MCNP_VERSION
+
+
+def test_cell_universe_nullify_with_problem():
+    import montepy
+
+    p = montepy.MCNP_Problem("tests/inputs/test.imcnp")
+    p.parse_input()
+    c = list(p.cells)[0]
+
+    assert c.universe.number != 0
+    c.universe = None
+    assert c.universe.number == 0
+
+    c = list(p.cells)[1]
+    assert c.universe.number == 0
+    del c.universe
+    assert c.universe.number == 0
+
+
+def test_cell_universe_nullify_adds_zero_universe():
+    import montepy
+
+    p = montepy.MCNP_Problem("out2.imcnp")
+    c = montepy.Cell()
+    c.number = 1
+    p.cells.append(c)
+
+    assert 0 not in p.universes.numbers
+    c.universe = None
+    assert 0 in p.universes.numbers
+    assert c.universe.number == 0
+
+
+def test_cell_universe_delete_adds_zero_universe():
+    import montepy
+
+    p = montepy.MCNP_Problem("out2.imcnp")
+    c = montepy.Cell()
+    c.number = 1
+    p.cells.append(c)
+
+    assert 0 not in p.universes.numbers
+    del c.universe
+    assert 0 in p.universes.numbers
+    assert c.universe.number == 0