Repository: scikit-hep/uproot5

Issue:
Cannot read TProfile2D or TProfile2D from file
### Setup information

```
uproot.__version__
'4.0.1rc2'
```
but also `4.0.0`

### Description

I opened a file,

```
filepath = "./bla.root"
data = uproot.open(filepath)
data["name"]
```

with `name` an object of type `TH1F`, `TH2F`, and `TProfile2D` respectively.

Only the latter couldn't be opened, resulting in the following error trace,

```python
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-21-b1df794c6084> in <module>
----> 1 data["DirLookupTable_degrees_type7"]

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/reading.py in __getitem__(self, where)
   1969 
   1970         else:
-> 1971             return self.key(where).get()
   1972 
   1973     @property

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/reading.py in get(self)
   2354 
   2355             try:
-> 2356                 out = cls.read(chunk, cursor, context, self._file, selffile, parent)
   2357 
   2358             except uproot.deserialization.DeserializationError:

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/model.py in read(cls, chunk, cursor, context, file, selffile, parent, concrete)
   1155 
   1156         elif num_bytes is not None:
-> 1157             versioned_cls = cls.new_class(file, version)
   1158 
   1159         elif context.get("in_TBranch", False):

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/model.py in new_class(cls, file, version)
   1094 
   1095         if streamer is not None:
-> 1096             versioned_cls = streamer.new_class(file)
   1097             versioned_cls.class_streamer = streamer
   1098             cls.known_versions[streamer.class_version] = versioned_cls

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/streamers.py in new_class(self, file)
    345         class_name = uproot.model.classname_encode(self.name, self.class_version)
    346         classes = uproot.model.maybe_custom_classes(file.custom_classes)
--> 347         return uproot.deserialization.compile_class(
    348             file, classes, class_code, class_name
    349         )

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/deserialization.py in compile_class(file, classes, class_code, class_name)
     77     setattr(uproot.dynamic, out.__name__, out)
     78 
---> 79     behaviors = tuple(_yield_all_behaviors(out, c))
     80     exclude = tuple(
     81         bad for cls in behaviors if hasattr(cls, "no_inherit") for bad in cls.no_inherit

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/deserialization.py in _yield_all_behaviors(cls, c)
     31 
     32 def _yield_all_behaviors(cls, c):
---> 33     behavior_cls = uproot.behavior_of(uproot.model.classname_decode(cls.__name__)[0])
     34     if behavior_cls is not None:
     35         yield behavior_cls

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/__init__.py in behavior_of(classname)
    194     if name not in globals():
    195         if name in behavior_of._module_names:
--> 196             exec(
    197                 compile(
    198                     "import uproot.behaviors.{0}".format(name), "<dynamic>", "exec"

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/__init__.py in <module>

~/Applications/miniconda3/envs/uproot4/lib/python3.9/site-packages/uproot/behaviors/TProfile2D.py in <module>
     13 
     14 
---> 15 class TProfile2D(uproot.behaviors.TProfile.Profile):
     16     """
     17     Behaviors for two-dimensional profiles: ROOT's ``TProfile2D``.

AttributeError: module 'uproot.behaviors' has no attribute 'TProfile'
```
### Further investigation

I created 2 dummy files with a `TProfile2D` and a `TProfile3D` (`TProfile` didn't manifest this error) with which I tested both the error (to exclude an issue with my particular data) and the proposed solution below,

[TProfile2D.txt](https://github.com/scikit-hep/uproot4/files/5682037/TProfile2D.txt)
[TProfile3D.txt](https://github.com/scikit-hep/uproot4/files/5682038/TProfile3D.txt)

I also leave you the code I used to make them,

[write_dummy_TProfile2D.txt](https://github.com/scikit-hep/uproot4/files/5682041/write_dummy_TProfile2D.txt)
[write_dummy_TProfile3D.txt](https://github.com/scikit-hep/uproot4/files/5682042/write_dummy_TProfile3D.txt)

### Proposed solution

I managed to solve the problem by modifying some imports statements in `TProfile2D.py`,

```python
from uproot.behaviors import TProfile, TH2

class TProfile2D(TProfile.Profile):

     no_inherit = (TH2.TH2,)
```

and `TProfile3D.py`,

```python
from uproot.behaviors import TProfile, TH3

class TProfile3D(TProfile.Profile):

    no_inherit = (TH3.TH3,)
```

If this is supposed to be the right way to do it, I'd be happy to contribute by opening a PR.
The dummy files could be also used to create unit-tests to open such ROOT objects.


Discussion:
Great, thanks for this! Since Uproot is an I/O library, we're limited in what we can test by the availability of files with certain features. I can (and have) made some files for testing, but mostly for corner cases. Files that come from real analyses are more valuable because there are a lot of knobs to turn in generating ROOT files, and we want to prioritize the ones that come from real analyses.

TProfile2D isn't being tested. You caught some (after the fact) obvious bugs and I'd be grateful if you submit a PR. You're not missing the "right way" to do this: you're doing it the right way; it's just broken.

In this case, all of the descendants of TH1 were stubbed out with "probably right" implementations to make sure that they didn't accidentally inherit methods intended for TH1. (This was a problem in Uproot 3; now we have a mechanism to prevent it, but preventing it means stubbing out the classes.)

I'll be available for any questions you have about the PR.
Oh, I prefer fully qualified names everywhere, so instead of

```python
from uproot.behaviors import TProfile, TH2

class TProfile2D(TProfile.Profile):
     no_inherit = (TH2.TH2,)
```

do

```python
import uproot.behaviors.TProfile
import uproot.behaviors.TH2

class TProfile2D(uproot.behaviors.TProfile.Profile):
     no_inherit = (uproot.behaviors.TH2.TH2,)
```
What should we do for the unit-test?

The files I produced are very simple dummy objects filled with random numbers and I am not quite sure how to write a function to check for "any" error.

Usually, a unit-test should assert or catch a specific error.
In this case was `AttributeError `, but in the future it could be anything.

Could something like the following work in general?

```
def test_read_TProfile(tmpdir):
    filename = os.path.join(str(tmpdir), "TProfile2D.root")

    with uproot.open(filename) as file:
            file["hprof3d"]
```
A bigger problem is how to include the file. The original Uproot repo became unwieldy because I added too many ROOT files—too many large diffs. So for this one, I've been putting the files into the https://github.com/scikit-hep/scikit-hep-testdata repo. If you're willing to do two pull requests, you could add your files to that one as I have been. Otherwise, you can forgo testing for now and I'll follow up.

As for the content of the test—after generation, the file has fixed contents, so you can check for exact numbers. Printing a Python list (not a NumPy array) includes enough decimals that you can paste those values into the text of a test and it will pass—no need for approximation. I did that with some of these, particularly the original TProfile ones, in which the numbers came from PyROOT, so that I could verify that Uproot was calculating exactly the same thing as ROOT. I don't think we need to do a full ROOT-to-Uproot closure for the 2-D and 3-D ones, since the bins are calculated the same way, just reshaped into a rectangle. Checking to make sure that the rectangle isn't flipped or transposed is a good idea—that was an error that we missed in the original TH2 tests (because our test samples were symmetric!).

Resolving patch:
diff --git a/uproot/behaviors/TProfile2D.py b/uproot/behaviors/TProfile2D.py
--- a/uproot/behaviors/TProfile2D.py
+++ b/uproot/behaviors/TProfile2D.py
@@ -10,6 +10,8 @@
 
 import uproot
 from uproot.behaviors.TH1 import boost_metadata, boost_axis_metadata
+import uproot.behaviors.TProfile
+import uproot.behaviors.TH2
 
 
 class TProfile2D(uproot.behaviors.TProfile.Profile):
diff --git a/uproot/behaviors/TProfile3D.py b/uproot/behaviors/TProfile3D.py
--- a/uproot/behaviors/TProfile3D.py
+++ b/uproot/behaviors/TProfile3D.py
@@ -10,6 +10,8 @@
 
 import uproot
 from uproot.behaviors.TH1 import boost_metadata, boost_axis_metadata
+import uproot.behaviors.TProfile
+import uproot.behaviors.TH3
 
 
 class TProfile3D(uproot.behaviors.TProfile.Profile):


Test patch:
diff --git a/tests/test_0228_read-TProfiles.py b/tests/test_0228_read-TProfiles.py
new file mode 100644
--- /dev/null
+++ b/tests/test_0228_read-TProfiles.py
@@ -0,0 +1,48 @@
+# BSD 3-Clause License; see https://github.com/scikit-hep/uproot4/blob/main/LICENSE
+
+import numpy as np
+from numpy.testing import assert_array_equal
+import skhep_testdata
+import uproot
+
+
+def test_read_TProfile2D():
+
+    file = skhep_testdata.data_path("uproot-issue-227a.root")
+
+    with uproot.open(file) as h:
+        T = h["hprof2d"]
+
+    assert T.kind == "MEAN"
+    assert_array_equal(T.axis("x").edges(), np.array([1., 2., 3.]))
+    assert_array_equal(T.axis("y").edges(), np.array([1., 2., 3., 4.]))
+    assert np.sum(T.counts(flow=True)) == 12
+    assert_array_equal(T.values().tolist(), [[1.0, 2.0, 0.0], [2.0, 4.0, 6.0]])
+
+
+def test_read_TProfile3D():
+
+    file = skhep_testdata.data_path("uproot-issue-227b.root")
+
+    with uproot.open(file) as h:
+        T = h["hprof3d"]
+
+    assert T.kind == "MEAN"
+    assert_array_equal(T.axis("x").edges(), np.array([1., 2., 3.]))
+    assert_array_equal(T.axis("y").edges(), np.array([1., 2., 3., 4.]))
+    assert_array_equal(T.axis("z").edges(), np.array([1., 2., 3., 4., 5.]))
+    assert np.sum(T.counts(flow=True)) == 12
+    assert_array_equal(
+        T.values().tolist(),
+        [
+            [
+                [2.0, 0.0, 0.0, 0.0],
+                [0.0, 4.0, 0.0, 0.0],
+                [0.0, 0.0, 6.0, 0.0]
+            ],
+            [
+                [0.0, 4.0, 0.0, 0.0],
+                [0.0, 0.0, 0.0, 8.0],
+                [0.0, 0.0, 0.0, 0.0]
+            ]
+        ])