Skip to content

feat: __dict__ major simplification #477

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Dec 21, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This sort of thing is no longer needed! :)

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__ = ()
Copy link
Member Author

@henryiii henryiii Dec 19, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was a bug (missing __slots__). But it wouldn’t be anymore!


@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