Skip to content

Commit ca298e7

Browse files
authored
feat: __dict__ major simplification (#477)
* 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
1 parent 513585e commit ca298e7

File tree

6 files changed

+149
-67
lines changed

6 files changed

+149
-67
lines changed

docs/CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ Pressing forward to 1.0.
99

1010
#### User changes
1111

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

1418
#### Bug fixes
1519

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

1924
#### Developer changes
2025

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

2834

2935
## Version 0.11

src/boost_histogram/_internal/axestuple.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def __getitem__(self, item):
9494
return self.__class__(result) if isinstance(result, tuple) else result
9595

9696
def __getattr__(self, attr):
97-
return self.__class__(s.__getattr__(attr) for s in self)
97+
return self.__class__(getattr(s, attr) for s in self)
9898

9999
def __setattr__(self, attr, values):
100100
return self.__class__(s.__setattr__(attr, v) for s, v in zip(self, values))

src/boost_histogram/_internal/axis.py

Lines changed: 105 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -30,35 +30,52 @@ def _isstr(value):
3030
# Contains common methods and properties to all axes
3131
@set_module("boost_histogram.axis")
3232
class Axis(object):
33-
__slots__ = ("_ax",)
33+
__slots__ = ("_ax", "__dict__")
3434

35-
def __copy__(self):
36-
other = self.__class__.__new__(self.__class__)
37-
other._ax = copy.copy(self._ax)
38-
return other
35+
def __setattr__(self, attr, value):
36+
if attr == "__dict__":
37+
self._ax.metadata = value
38+
object.__setattr__(self, attr, value)
3939

40-
def __getattr__(self, item):
41-
if item == "_ax":
42-
return Axis.__dict__[item].__get__(self)
43-
elif item in self._ax.metadata:
44-
return self._ax.metadata[item]
45-
elif item == "metadata":
40+
def __getattr__(self, attr):
41+
if attr == "metadata":
4642
return None
47-
else:
48-
msg = "'{}' object has no attribute '{}' in {}".format(
49-
type(self).__name__, item, set(self._ax.metadata)
43+
raise AttributeError(
44+
"object {0} has not attribute {1}".format(self.__class__.__name__, attr)
45+
)
46+
47+
def __init__(self, ax, metadata, __dict__):
48+
"""
49+
ax: the C++ object
50+
metadata: the metadata keyword contents
51+
__dict__: the __dict__ keyword contents
52+
"""
53+
54+
self._ax = ax
55+
56+
if __dict__ is not None and metadata is not None:
57+
raise KeyError(
58+
"Cannot provide metadata by keyword and __dict__, use __dict__ only"
5059
)
51-
raise AttributeError(msg)
60+
elif __dict__ is not None:
61+
self._ax.metadata = __dict__
62+
elif metadata is not None:
63+
self._ax.metadata["metadata"] = metadata
5264

53-
def __setattr__(self, item, value):
54-
if item == "_ax":
55-
Axis.__dict__[item].__set__(self, value)
56-
else:
57-
self._ax.metadata[item] = value
65+
self.__dict__ = self._ax.metadata
66+
67+
def __setstate__(self, state):
68+
self._ax = state["_ax"]
69+
self.__dict__ = self._ax.metadata
70+
71+
def __getstate__(self):
72+
return {"_ax": self._ax}
5873

59-
def __dir__(self):
60-
metadata = list(self._ax.metadata)
61-
return sorted(dir(type(self)) + metadata)
74+
def __copy__(self):
75+
other = self.__class__.__new__(self.__class__)
76+
other._ax = copy.copy(self._ax)
77+
other.__dict__ = other._ax.metadata
78+
return other
6279

6380
def index(self, value):
6481
"""
@@ -100,6 +117,7 @@ def __ne__(self, other):
100117
def _convert_cpp(cls, cpp_object):
101118
nice_ax = cls.__new__(cls)
102119
nice_ax._ax = cpp_object
120+
nice_ax.__dict__ = cpp_object.metadata
103121
return nice_ax
104122

105123
def __len__(self):
@@ -230,7 +248,7 @@ class Regular(Axis):
230248
__slots__ = ()
231249

232250
@inject_signature(
233-
"self, bins, start, stop, *, metadata=None, underflow=True, overflow=True, growth=False, circular=False, transform=None"
251+
"self, bins, start, stop, *, metadata=None, underflow=True, overflow=True, growth=False, circular=False, transform=None, __dict__=None"
234252
)
235253
def __init__(self, bins, start, stop, **kwargs):
236254
"""
@@ -258,11 +276,14 @@ def __init__(self, bins, start, stop, **kwargs):
258276
Filling wraps around.
259277
transform : Optional[AxisTransform] = None
260278
Transform the regular bins (Log, Sqrt, and Pow(v))
279+
__dict__: Optional[Dict[str, Any]] = None
280+
The full metadata dictionary
261281
"""
262282

263283
with KWArgs(kwargs) as k:
264284
metadata = k.optional("metadata")
265285
transform = k.optional("transform")
286+
__dict__ = k.optional("__dict__")
266287
options = k.options(
267288
underflow=True, overflow=True, growth=False, circular=False
268289
)
@@ -277,29 +298,29 @@ def __init__(self, bins, start, stop, **kwargs):
277298
):
278299
raise TypeError("You must pass an instance, use {}()".format(transform))
279300

280-
self._ax = transform._produce(bins, start, stop)
301+
ax = transform._produce(bins, start, stop)
281302

282303
elif options == {"growth", "underflow", "overflow"}:
283-
self._ax = ca.regular_uoflow_growth(bins, start, stop)
304+
ax = ca.regular_uoflow_growth(bins, start, stop)
284305
elif options == {"underflow", "overflow"}:
285-
self._ax = ca.regular_uoflow(bins, start, stop)
306+
ax = ca.regular_uoflow(bins, start, stop)
286307
elif options == {"underflow"}:
287-
self._ax = ca.regular_uflow(bins, start, stop)
308+
ax = ca.regular_uflow(bins, start, stop)
288309
elif options == {"overflow"}:
289-
self._ax = ca.regular_oflow(bins, start, stop)
310+
ax = ca.regular_oflow(bins, start, stop)
290311
elif options == {"circular", "underflow", "overflow"} or options == {
291312
"circular",
292313
"overflow",
293314
}:
294315
# growth=True, underflow=False is also correct
295-
self._ax = ca.regular_circular(bins, start, stop)
316+
ax = ca.regular_circular(bins, start, stop)
296317

297318
elif options == set():
298-
self._ax = ca.regular_none(bins, start, stop)
319+
ax = ca.regular_none(bins, start, stop)
299320
else:
300321
raise KeyError("Unsupported collection of options")
301322

302-
self.metadata = metadata
323+
super(Regular, self).__init__(ax, metadata, __dict__)
303324

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

341362
@inject_signature(
342-
"self, edges, *, metadata=None, underflow=True, overflow=True, growth=False"
363+
"self, edges, *, metadata=None, underflow=True, overflow=True, growth=False, __dict__=None"
343364
)
344365
def __init__(self, edges, **kwargs):
345366
"""
@@ -361,33 +382,37 @@ def __init__(self, edges, **kwargs):
361382
growth : bool = False
362383
Allow the axis to grow if a value is encountered out of range.
363384
Be careful, the axis will grow as large as needed.
385+
__dict__: Optional[Dict[str, Any]] = None
386+
The full metadata dictionary
364387
"""
388+
365389
with KWArgs(kwargs) as k:
366390
metadata = k.optional("metadata")
391+
__dict__ = k.optional("__dict__")
367392
options = k.options(
368393
underflow=True, overflow=True, circular=False, growth=False
369394
)
370395

371396
if options == {"growth", "underflow", "overflow"}:
372-
self._ax = ca.variable_uoflow_growth(edges)
397+
ax = ca.variable_uoflow_growth(edges)
373398
elif options == {"underflow", "overflow"}:
374-
self._ax = ca.variable_uoflow(edges)
399+
ax = ca.variable_uoflow(edges)
375400
elif options == {"underflow"}:
376-
self._ax = ca.variable_uflow(edges)
401+
ax = ca.variable_uflow(edges)
377402
elif options == {"overflow"}:
378-
self._ax = ca.variable_oflow(edges)
403+
ax = ca.variable_oflow(edges)
379404
elif options == {"circular", "underflow", "overflow",} or options == {
380405
"circular",
381406
"overflow",
382407
}:
383408
# growth=True, underflow=False is also correct
384-
self._ax = ca.variable_circular(edges)
409+
ax = ca.variable_circular(edges)
385410
elif options == set():
386-
self._ax = ca.variable_none(edges)
411+
ax = ca.variable_none(edges)
387412
else:
388413
raise KeyError("Unsupported collection of options")
389414

390-
self.metadata = metadata
415+
super(Variable, self).__init__(ax, metadata, __dict__)
391416

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

416441
@inject_signature(
417-
"self, start, stop, *, metadata=None, underflow=True, overflow=True, growth=False"
442+
"self, start, stop, *, metadata=None, underflow=True, overflow=True, growth=False, __dict__=None"
418443
)
419444
def __init__(self, start, stop, **kwargs):
420445
"""
@@ -437,31 +462,35 @@ def __init__(self, start, stop, **kwargs):
437462
growth : bool = False
438463
Allow the axis to grow if a value is encountered out of range.
439464
Be careful, the axis will grow as large as needed.
465+
__dict__: Optional[Dict[str, Any]] = None
466+
The full metadata dictionary
440467
"""
468+
441469
with KWArgs(kwargs) as k:
442470
metadata = k.optional("metadata")
471+
__dict__ = k.optional("__dict__")
443472
options = k.options(
444473
underflow=True, overflow=True, circular=False, growth=False
445474
)
446475

447476
# underflow and overflow settings are ignored, integers are always
448477
# finite and thus cannot end up in a flow bin when growth is on
449478
if "growth" in options and "circular" not in options:
450-
self._ax = ca.integer_growth(start, stop)
479+
ax = ca.integer_growth(start, stop)
451480
elif options == {"underflow", "overflow"}:
452-
self._ax = ca.integer_uoflow(start, stop)
481+
ax = ca.integer_uoflow(start, stop)
453482
elif options == {"underflow"}:
454-
self._ax = ca.integer_uflow(start, stop)
483+
ax = ca.integer_uflow(start, stop)
455484
elif options == {"overflow"}:
456-
self._ax = ca.integer_oflow(start, stop)
485+
ax = ca.integer_oflow(start, stop)
457486
elif "circular" in options and "growth" not in options:
458-
self._ax = ca.integer_circular(start, stop)
487+
ax = ca.integer_circular(start, stop)
459488
elif options == set():
460-
self._ax = ca.integer_none(start, stop)
489+
ax = ca.integer_none(start, stop)
461490
else:
462491
raise KeyError("Unsupported collection of options")
463492

464-
self.metadata = metadata
493+
super(Integer, self).__init__(ax, metadata, __dict__)
465494

466495
def _repr_args(self):
467496
"Return inner part of signature for use in repr"
@@ -495,7 +524,9 @@ def _repr_kwargs(self):
495524
@set_module("boost_histogram.axis")
496525
@register({ca.category_str_growth, ca.category_str})
497526
class StrCategory(BaseCategory):
498-
@inject_signature("self, categories, *, metadata=None, growth=False")
527+
__slots__ = ()
528+
529+
@inject_signature("self, categories, *, metadata=None, growth=False, __dict__=None")
499530
def __init__(self, categories, **kwargs):
500531
"""
501532
Make a category axis with strings; items will
@@ -512,22 +543,26 @@ def __init__(self, categories, **kwargs):
512543
growth : bool = False
513544
Allow the axis to grow if a value is encountered out of range.
514545
Be careful, the axis will grow as large as needed.
546+
__dict__: Optional[Dict[str, Any]] = None
547+
The full metadata dictionary
515548
"""
549+
516550
with KWArgs(kwargs) as k:
517551
metadata = k.optional("metadata")
552+
__dict__ = k.optional("__dict__")
518553
options = k.options(growth=False)
519554

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

523558
if options == {"growth"}:
524-
self._ax = ca.category_str_growth(tuple(categories))
559+
ax = ca.category_str_growth(tuple(categories))
525560
elif options == set():
526-
self._ax = ca.category_str(tuple(categories))
561+
ax = ca.category_str(tuple(categories))
527562
else:
528563
raise KeyError("Unsupported collection of options")
529564

530-
self.metadata = metadata
565+
super(StrCategory, self).__init__(ax, metadata, __dict__)
531566

532567
def index(self, value):
533568
"""
@@ -553,7 +588,9 @@ def _repr_args(self):
553588
@set_module("boost_histogram.axis")
554589
@register({ca.category_int, ca.category_int_growth})
555590
class IntCategory(BaseCategory):
556-
@inject_signature("self, categories, *, metadata=None, growth=False")
591+
__slots__ = ()
592+
593+
@inject_signature("self, categories, *, metadata=None, growth=False, __dict__=None")
557594
def __init__(self, categories, **kwargs):
558595
"""
559596
Make a category axis with ints; items will
@@ -570,19 +607,23 @@ def __init__(self, categories, **kwargs):
570607
growth : bool = False
571608
Allow the axis to grow if a value is encountered out of range.
572609
Be careful, the axis will grow as large as needed.
610+
__dict__: Optional[Dict[str, Any]] = None
611+
The full metadata dictionary
573612
"""
613+
574614
with KWArgs(kwargs) as k:
575615
metadata = k.optional("metadata")
616+
__dict__ = k.optional("__dict__")
576617
options = k.options(growth=False)
577618

578619
if options == {"growth"}:
579-
self._ax = ca.category_int_growth(tuple(categories))
620+
ax = ca.category_int_growth(tuple(categories))
580621
elif options == set():
581-
self._ax = ca.category_int(tuple(categories))
622+
ax = ca.category_int(tuple(categories))
582623
else:
583624
raise KeyError("Unsupported collection of options")
584625

585-
self.metadata = metadata
626+
super(IntCategory, self).__init__(ax, metadata, __dict__)
586627

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

600-
@inject_signature("self, *, metadata=None")
641+
@inject_signature("self, *, metadata=None, __dict__=None")
601642
def __init__(self, **kwargs):
602643
"""
603644
Make an axis for boolean values.
@@ -606,12 +647,17 @@ def __init__(self, **kwargs):
606647
----------
607648
metadata : object
608649
Any Python object to attach to the axis, like a label.
650+
__dict__: Optional[Dict[str, Any]] = None
651+
The full metadata dictionary
609652
"""
653+
610654
with KWArgs(kwargs) as k:
611655
metadata = k.optional("metadata")
656+
__dict__ = k.optional("__dict__")
657+
658+
ax = ca.boolean()
612659

613-
self._ax = ca.boolean()
614-
self.metadata = metadata
660+
super(Boolean, self).__init__(ax, metadata, __dict__)
615661

616662
def _repr_args(self):
617663
"Return inner part of signature for use in repr"

0 commit comments

Comments
 (0)