Skip to content

Commit

Permalink
feat: __dict__ major simplification (#477)
Browse files Browse the repository at this point in the history
* feat: __dict__ (WIP)

* fix: python 2 support

* feat: support all other axes types

* fix: test __dict__ and fix bug with categories missing slots

* fix: force subclasses to have __slots__

* refactor!: real __dict__ instead of facade!

* refactor: minor cleanup
  • Loading branch information
henryiii committed Dec 21, 2020
1 parent 513585e commit ca298e7
Show file tree
Hide file tree
Showing 6 changed files with 149 additions and 67 deletions.
8 changes: 7 additions & 1 deletion docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ Pressing forward to 1.0.

#### User changes

* You can now set all complex storages, either on a Histogram or a View with an (N+1)D array [#475][]
* You can now set all complex storages, either on a Histogram or a View with an
(N+1)D array [#475][]
* Axes are now normal `__dict__` classes, you can manipulate the `__dict__` as
normal. Axes construction now lets you either use the old metadata shortcut
or the `__dict__` inline. [#477][]

#### Bug fixes

* Fixed issue if final bin of Variable histogram was infinite by updating to Boost 1.75 [#470][]
* NumPy arrays can be used for weights in `bh.numpy` [#472][]
* Vectorization for WeightedMean accumulators was broken [#475][]

#### Developer changes

Expand All @@ -24,6 +29,7 @@ Pressing forward to 1.0.
[#470]: https://github.com/scikit-hep/boost-histogram/pull/470
[#472]: https://github.com/scikit-hep/boost-histogram/pull/472
[#475]: https://github.com/scikit-hep/boost-histogram/pull/475
[#477]: https://github.com/scikit-hep/boost-histogram/pull/477


## Version 0.11
Expand Down
2 changes: 1 addition & 1 deletion src/boost_histogram/_internal/axestuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def __getitem__(self, item):
return self.__class__(result) if isinstance(result, tuple) else result

def __getattr__(self, attr):
return self.__class__(s.__getattr__(attr) for s in self)
return self.__class__(getattr(s, attr) for s in self)

def __setattr__(self, attr, values):
return self.__class__(s.__setattr__(attr, v) for s, v in zip(self, values))
Expand Down
164 changes: 105 additions & 59 deletions src/boost_histogram/_internal/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,35 +30,52 @@ def _isstr(value):
# Contains common methods and properties to all axes
@set_module("boost_histogram.axis")
class Axis(object):
__slots__ = ("_ax",)
__slots__ = ("_ax", "__dict__")

def __copy__(self):
other = self.__class__.__new__(self.__class__)
other._ax = copy.copy(self._ax)
return other
def __setattr__(self, attr, value):
if attr == "__dict__":
self._ax.metadata = value
object.__setattr__(self, attr, value)

def __getattr__(self, item):
if item == "_ax":
return Axis.__dict__[item].__get__(self)
elif item in self._ax.metadata:
return self._ax.metadata[item]
elif item == "metadata":
def __getattr__(self, attr):
if attr == "metadata":
return None
else:
msg = "'{}' object has no attribute '{}' in {}".format(
type(self).__name__, item, set(self._ax.metadata)
raise AttributeError(
"object {0} has not attribute {1}".format(self.__class__.__name__, attr)
)

def __init__(self, ax, metadata, __dict__):
"""
ax: the C++ object
metadata: the metadata keyword contents
__dict__: the __dict__ keyword contents
"""

self._ax = ax

if __dict__ is not None and metadata is not None:
raise KeyError(
"Cannot provide metadata by keyword and __dict__, use __dict__ only"
)
raise AttributeError(msg)
elif __dict__ is not None:
self._ax.metadata = __dict__
elif metadata is not None:
self._ax.metadata["metadata"] = metadata

def __setattr__(self, item, value):
if item == "_ax":
Axis.__dict__[item].__set__(self, value)
else:
self._ax.metadata[item] = value
self.__dict__ = self._ax.metadata

def __setstate__(self, state):
self._ax = state["_ax"]
self.__dict__ = self._ax.metadata

def __getstate__(self):
return {"_ax": self._ax}

def __dir__(self):
metadata = list(self._ax.metadata)
return sorted(dir(type(self)) + metadata)
def __copy__(self):
other = self.__class__.__new__(self.__class__)
other._ax = copy.copy(self._ax)
other.__dict__ = other._ax.metadata
return other

def index(self, value):
"""
Expand Down Expand Up @@ -100,6 +117,7 @@ def __ne__(self, other):
def _convert_cpp(cls, cpp_object):
nice_ax = cls.__new__(cls)
nice_ax._ax = cpp_object
nice_ax.__dict__ = cpp_object.metadata
return nice_ax

def __len__(self):
Expand Down Expand Up @@ -230,7 +248,7 @@ class Regular(Axis):
__slots__ = ()

@inject_signature(
"self, bins, start, stop, *, metadata=None, underflow=True, overflow=True, growth=False, circular=False, transform=None"
"self, bins, start, stop, *, metadata=None, underflow=True, overflow=True, growth=False, circular=False, transform=None, __dict__=None"
)
def __init__(self, bins, start, stop, **kwargs):
"""
Expand Down Expand Up @@ -258,11 +276,14 @@ def __init__(self, bins, start, stop, **kwargs):
Filling wraps around.
transform : Optional[AxisTransform] = None
Transform the regular bins (Log, Sqrt, and Pow(v))
__dict__: Optional[Dict[str, Any]] = None
The full metadata dictionary
"""

with KWArgs(kwargs) as k:
metadata = k.optional("metadata")
transform = k.optional("transform")
__dict__ = k.optional("__dict__")
options = k.options(
underflow=True, overflow=True, growth=False, circular=False
)
Expand All @@ -277,29 +298,29 @@ def __init__(self, bins, start, stop, **kwargs):
):
raise TypeError("You must pass an instance, use {}()".format(transform))

self._ax = transform._produce(bins, start, stop)
ax = transform._produce(bins, start, stop)

elif options == {"growth", "underflow", "overflow"}:
self._ax = ca.regular_uoflow_growth(bins, start, stop)
ax = ca.regular_uoflow_growth(bins, start, stop)
elif options == {"underflow", "overflow"}:
self._ax = ca.regular_uoflow(bins, start, stop)
ax = ca.regular_uoflow(bins, start, stop)
elif options == {"underflow"}:
self._ax = ca.regular_uflow(bins, start, stop)
ax = ca.regular_uflow(bins, start, stop)
elif options == {"overflow"}:
self._ax = ca.regular_oflow(bins, start, stop)
ax = ca.regular_oflow(bins, start, stop)
elif options == {"circular", "underflow", "overflow"} or options == {
"circular",
"overflow",
}:
# growth=True, underflow=False is also correct
self._ax = ca.regular_circular(bins, start, stop)
ax = ca.regular_circular(bins, start, stop)

elif options == set():
self._ax = ca.regular_none(bins, start, stop)
ax = ca.regular_none(bins, start, stop)
else:
raise KeyError("Unsupported collection of options")

self.metadata = metadata
super(Regular, self).__init__(ax, metadata, __dict__)

def _repr_args(self):
"Return inner part of signature for use in repr"
Expand Down Expand Up @@ -339,7 +360,7 @@ class Variable(Axis):
__slots__ = ()

@inject_signature(
"self, edges, *, metadata=None, underflow=True, overflow=True, growth=False"
"self, edges, *, metadata=None, underflow=True, overflow=True, growth=False, __dict__=None"
)
def __init__(self, edges, **kwargs):
"""
Expand All @@ -361,33 +382,37 @@ def __init__(self, edges, **kwargs):
growth : bool = False
Allow the axis to grow if a value is encountered out of range.
Be careful, the axis will grow as large as needed.
__dict__: Optional[Dict[str, Any]] = None
The full metadata dictionary
"""

with KWArgs(kwargs) as k:
metadata = k.optional("metadata")
__dict__ = k.optional("__dict__")
options = k.options(
underflow=True, overflow=True, circular=False, growth=False
)

if options == {"growth", "underflow", "overflow"}:
self._ax = ca.variable_uoflow_growth(edges)
ax = ca.variable_uoflow_growth(edges)
elif options == {"underflow", "overflow"}:
self._ax = ca.variable_uoflow(edges)
ax = ca.variable_uoflow(edges)
elif options == {"underflow"}:
self._ax = ca.variable_uflow(edges)
ax = ca.variable_uflow(edges)
elif options == {"overflow"}:
self._ax = ca.variable_oflow(edges)
ax = ca.variable_oflow(edges)
elif options == {"circular", "underflow", "overflow",} or options == {
"circular",
"overflow",
}:
# growth=True, underflow=False is also correct
self._ax = ca.variable_circular(edges)
ax = ca.variable_circular(edges)
elif options == set():
self._ax = ca.variable_none(edges)
ax = ca.variable_none(edges)
else:
raise KeyError("Unsupported collection of options")

self.metadata = metadata
super(Variable, self).__init__(ax, metadata, __dict__)

def _repr_args(self):
"Return inner part of signature for use in repr"
Expand All @@ -414,7 +439,7 @@ class Integer(Axis):
__slots__ = ()

@inject_signature(
"self, start, stop, *, metadata=None, underflow=True, overflow=True, growth=False"
"self, start, stop, *, metadata=None, underflow=True, overflow=True, growth=False, __dict__=None"
)
def __init__(self, start, stop, **kwargs):
"""
Expand All @@ -437,31 +462,35 @@ def __init__(self, start, stop, **kwargs):
growth : bool = False
Allow the axis to grow if a value is encountered out of range.
Be careful, the axis will grow as large as needed.
__dict__: Optional[Dict[str, Any]] = None
The full metadata dictionary
"""

with KWArgs(kwargs) as k:
metadata = k.optional("metadata")
__dict__ = k.optional("__dict__")
options = k.options(
underflow=True, overflow=True, circular=False, growth=False
)

# underflow and overflow settings are ignored, integers are always
# finite and thus cannot end up in a flow bin when growth is on
if "growth" in options and "circular" not in options:
self._ax = ca.integer_growth(start, stop)
ax = ca.integer_growth(start, stop)
elif options == {"underflow", "overflow"}:
self._ax = ca.integer_uoflow(start, stop)
ax = ca.integer_uoflow(start, stop)
elif options == {"underflow"}:
self._ax = ca.integer_uflow(start, stop)
ax = ca.integer_uflow(start, stop)
elif options == {"overflow"}:
self._ax = ca.integer_oflow(start, stop)
ax = ca.integer_oflow(start, stop)
elif "circular" in options and "growth" not in options:
self._ax = ca.integer_circular(start, stop)
ax = ca.integer_circular(start, stop)
elif options == set():
self._ax = ca.integer_none(start, stop)
ax = ca.integer_none(start, stop)
else:
raise KeyError("Unsupported collection of options")

self.metadata = metadata
super(Integer, self).__init__(ax, metadata, __dict__)

def _repr_args(self):
"Return inner part of signature for use in repr"
Expand Down Expand Up @@ -495,7 +524,9 @@ def _repr_kwargs(self):
@set_module("boost_histogram.axis")
@register({ca.category_str_growth, ca.category_str})
class StrCategory(BaseCategory):
@inject_signature("self, categories, *, metadata=None, growth=False")
__slots__ = ()

@inject_signature("self, categories, *, metadata=None, growth=False, __dict__=None")
def __init__(self, categories, **kwargs):
"""
Make a category axis with strings; items will
Expand All @@ -512,22 +543,26 @@ def __init__(self, categories, **kwargs):
growth : bool = False
Allow the axis to grow if a value is encountered out of range.
Be careful, the axis will grow as large as needed.
__dict__: Optional[Dict[str, Any]] = None
The full metadata dictionary
"""

with KWArgs(kwargs) as k:
metadata = k.optional("metadata")
__dict__ = k.optional("__dict__")
options = k.options(growth=False)

# henryiii: We currently expand "abc" to "a", "b", "c" - some
# Python interfaces protect against that

if options == {"growth"}:
self._ax = ca.category_str_growth(tuple(categories))
ax = ca.category_str_growth(tuple(categories))
elif options == set():
self._ax = ca.category_str(tuple(categories))
ax = ca.category_str(tuple(categories))
else:
raise KeyError("Unsupported collection of options")

self.metadata = metadata
super(StrCategory, self).__init__(ax, metadata, __dict__)

def index(self, value):
"""
Expand All @@ -553,7 +588,9 @@ def _repr_args(self):
@set_module("boost_histogram.axis")
@register({ca.category_int, ca.category_int_growth})
class IntCategory(BaseCategory):
@inject_signature("self, categories, *, metadata=None, growth=False")
__slots__ = ()

@inject_signature("self, categories, *, metadata=None, growth=False, __dict__=None")
def __init__(self, categories, **kwargs):
"""
Make a category axis with ints; items will
Expand All @@ -570,19 +607,23 @@ def __init__(self, categories, **kwargs):
growth : bool = False
Allow the axis to grow if a value is encountered out of range.
Be careful, the axis will grow as large as needed.
__dict__: Optional[Dict[str, Any]] = None
The full metadata dictionary
"""

with KWArgs(kwargs) as k:
metadata = k.optional("metadata")
__dict__ = k.optional("__dict__")
options = k.options(growth=False)

if options == {"growth"}:
self._ax = ca.category_int_growth(tuple(categories))
ax = ca.category_int_growth(tuple(categories))
elif options == set():
self._ax = ca.category_int(tuple(categories))
ax = ca.category_int(tuple(categories))
else:
raise KeyError("Unsupported collection of options")

self.metadata = metadata
super(IntCategory, self).__init__(ax, metadata, __dict__)

def _repr_args(self):
"Return inner part of signature for use in repr"
Expand All @@ -597,7 +638,7 @@ def _repr_args(self):
class Boolean(Axis):
__slots__ = ()

@inject_signature("self, *, metadata=None")
@inject_signature("self, *, metadata=None, __dict__=None")
def __init__(self, **kwargs):
"""
Make an axis for boolean values.
Expand All @@ -606,12 +647,17 @@ def __init__(self, **kwargs):
----------
metadata : object
Any Python object to attach to the axis, like a label.
__dict__: Optional[Dict[str, Any]] = None
The full metadata dictionary
"""

with KWArgs(kwargs) as k:
metadata = k.optional("metadata")
__dict__ = k.optional("__dict__")

ax = ca.boolean()

self._ax = ca.boolean()
self.metadata = metadata
super(Boolean, self).__init__(ax, metadata, __dict__)

def _repr_args(self):
"Return inner part of signature for use in repr"
Expand Down
Loading

0 comments on commit ca298e7

Please sign in to comment.