diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2b11840c4..9508233f1 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -31,6 +31,18 @@ jobs: - name: Build run: cmake --build build -j 2 + pylint: + runs-on: ubuntu-latest + name: PyLint + + steps: + - uses: actions/checkout@v1 + with: + submodules: true + + - name: Run PyLint + run: pipx run nox -s pylint + cmake: runs-on: ubuntu-latest diff --git a/noxfile.py b/noxfile.py index f3c2d715c..e2e837b4c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -41,6 +41,17 @@ def lint(session: nox.Session) -> None: session.run("pre-commit", "run", "--all-files", *session.posargs) +@nox.session +def pylint(session: nox.Session) -> None: + """ + Run pylint. + """ + + session.install("pylint") + session.install("-e", ".") + session.run("pylint", "src") + + @nox.session def make_pickle(session: nox.Session) -> None: """ diff --git a/pyproject.toml b/pyproject.toml index 5fa6a1431..76997b04b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -85,3 +85,28 @@ manylinux-i686-image = "manylinux2014" select = "cp3?-*" manylinux-x86_64-image = "manylinux2010" manylinux-i686-image = "manylinux2010" + +[tool.pylint] + +master.py-version = "3.6" +master.extension-pkg-allow-list = ["boost_histogram._core"] +similarities.ignore-imports = "yes" +messages_control.disable = [ + "fixme", + "invalid-name", + "line-too-long", + "missing-class-docstring", + "missing-function-docstring", + "missing-module-docstring", + "no-member", # C extensions mess with this + "protected-access", + "too-few-public-methods", + "too-many-arguments", + "too-many-branches", + "too-many-lines", + "too-many-locals", + "too-many-return-statements", + "too-many-statements", + "wrong-import-position", + "cyclic-import", # TODO: move files out of _internal +] diff --git a/src/boost_histogram/__init__.py b/src/boost_histogram/__init__.py index 9f7095e04..8320d2ea7 100644 --- a/src/boost_histogram/__init__.py +++ b/src/boost_histogram/__init__.py @@ -1,7 +1,13 @@ from . import accumulators, axis, numpy, storage from ._internal.enum import Kind from ._internal.hist import Histogram, IndexingExpr -from .tag import loc, overflow, rebin, sum, underflow +from .tag import ( # pylint: disable=redefined-builtin + loc, + overflow, + rebin, + sum, + underflow, +) from .version import version as __version__ try: diff --git a/src/boost_histogram/_internal/axestuple.py b/src/boost_histogram/_internal/axestuple.py index de6b4fdad..560930b3e 100644 --- a/src/boost_histogram/_internal/axestuple.py +++ b/src/boost_histogram/_internal/axestuple.py @@ -18,8 +18,8 @@ class ArrayTuple(tuple): # type: ignore[type-arg] def __getattr__(self, name: str) -> Any: if name in self._REDUCTIONS: return partial(getattr(np, name), np.broadcast_arrays(*self)) # type: ignore[no-untyped-call] - else: - return self.__class__(getattr(a, name) for a in self) + + return self.__class__(getattr(a, name) for a in self) def __dir__(self) -> List[str]: names = dir(self.__class__) + dir("np.typing.NDArray[Any]") @@ -51,6 +51,7 @@ def __init__(self, __iterable: Iterable[Axis]) -> None: raise TypeError( f"Only an iterable of Axis supported in AxesTuple, got {item}" ) + super().__init__() @property def size(self) -> Tuple[int, ...]: @@ -105,7 +106,7 @@ def __getattr__(self, attr: str) -> Tuple[Any, ...]: def __setattr__(self, attr: str, values: Any) -> None: try: - return super().__setattr__(attr, values) + super().__setattr__(attr, values) except AttributeError: for s, v in zip_strict(self, values): s.__setattr__(attr, v) diff --git a/src/boost_histogram/_internal/axis.py b/src/boost_histogram/_internal/axis.py index 9bfb3e4f2..469da4b1d 100644 --- a/src/boost_histogram/_internal/axis.py +++ b/src/boost_histogram/_internal/axis.py @@ -14,7 +14,7 @@ Union, ) -import numpy as np +import numpy as np # pylint: disable=unused-import import boost_histogram @@ -31,10 +31,9 @@ def _isstr(value: Any) -> bool: if isinstance(value, (str, bytes)): return True - elif hasattr(value, "__iter__"): + if hasattr(value, "__iter__"): return all(_isstr(v) for v in value) - else: - return False + return False def _opts(**kwargs: bool) -> Set[str]: @@ -87,7 +86,7 @@ def __init__( raise KeyError( "Cannot provide metadata by keyword and __dict__, use __dict__ only" ) - elif __dict__ is not None: + if __dict__ is not None: self._ax.metadata = __dict__ elif metadata is not None: self._ax.metadata["metadata"] = metadata @@ -112,14 +111,11 @@ def index(self, value: Union[float, str]) -> int: Return the fractional index(es) given a value (or values) on the axis. """ - if not _isstr(value): - return self._ax.index(value) # type: ignore[no-any-return] - else: - raise TypeError( - "index({value}) cannot be a string for a numerical axis".format( - value=value - ) - ) + if _isstr(value): + msg = f"index({value}) cannot be a string for a numerical axis" + raise TypeError(msg) + + return self._ax.index(value) # type: ignore[no-any-return] def value(self, index: float) -> float: """ @@ -486,7 +482,8 @@ def _repr_args_(self) -> List[str]: if len(self) > 20: ret = [repr(self.edges)] else: - ret = ["[{}]".format(", ".join(format(v, "g") for v in self.edges))] + args = ", ".join(format(v, "g") for v in self.edges) + ret = [f"[{args}]"] if self.traits.growth: ret.append("growth=True") @@ -670,17 +667,15 @@ def index(self, value: Union[float, str]) -> int: if _isstr(value): return self._ax.index(value) # type: ignore[no-any-return] - else: - raise TypeError( - "index({value}) must be a string or iterable of strings for a StrCategory axis".format( - value=value - ) - ) + + msg = f"index({value}) must be a string or iterable of strings for a StrCategory axis" + raise TypeError(msg) def _repr_args_(self) -> List[str]: "Return inner part of signature for use in repr" - ret = ["[{}]".format(", ".join(repr(c) for c in self))] + args = ", ".join(repr(c) for c in self) + ret = [f"[{args}]"] ret += super()._repr_args_() return ret @@ -732,7 +727,8 @@ def __init__( def _repr_args_(self) -> List[str]: "Return inner part of signature for use in repr" - ret = ["[{}]".format(", ".join(format(c, "g") for c in self))] + args = ", ".join(format(c, "g") for c in self) + ret = [f"[{args}]"] ret += super()._repr_args_() return ret diff --git a/src/boost_histogram/_internal/axis_transform.py b/src/boost_histogram/_internal/axis_transform.py index 8ed256fa1..3ccbfcb36 100644 --- a/src/boost_histogram/_internal/axis_transform.py +++ b/src/boost_histogram/_internal/axis_transform.py @@ -33,8 +33,8 @@ def _convert_cpp(cls: Type[T], this: Any) -> T: def __repr__(self) -> str: if hasattr(self, "_this"): return repr(self._this) - else: - return f"{self.__class__.__name__}() # Missing _this, broken class" + + return f"{self.__class__.__name__}() # Missing _this, broken class" def _produce(self, bins: int, start: float, stop: float) -> Any: # Note: this is an ABC; _type must be defined on children @@ -62,7 +62,7 @@ class Pow(AxisTransform, family=boost_histogram): __slots__ = () _type = ca.regular_pow - def __init__(self, power: float): + def __init__(self, power: float): # pylint: disable=super-init-not-called "Create a new transform instance" # Note: this comes from family (cpp_class,) = self._types # type: ignore[attr-defined] @@ -84,7 +84,7 @@ class Function(AxisTransform, family=boost_histogram): __slots__ = () _type = ca.regular_trans - def __init__( + def __init__( # pylint: disable=super-init-not-called self, forward: Any, inverse: Any, *, convert: Any = None, name: str = "" ): """ diff --git a/src/boost_histogram/_internal/deprecated.py b/src/boost_histogram/_internal/deprecated.py index 3ac84da24..363d993b0 100644 --- a/src/boost_histogram/_internal/deprecated.py +++ b/src/boost_histogram/_internal/deprecated.py @@ -17,13 +17,11 @@ def __call__(self, func: Any) -> Any: @functools.wraps(func) def decorated_func(*args: Any, **kwargs: Any) -> Any: warnings.warn( - "{} is deprecated: {}".format( - self._name or func.__name__, self._reason - ), + f"{self._name or func.__name__} is deprecated: {self._reason}", category=FutureWarning, stacklevel=2, ) return func(*args, **kwargs) - decorated_func.__doc__ = "DEPRECATED: " + self._reason + "\n" + func.__doc__ + decorated_func.__doc__ = f"DEPRECATED: {self._reason}\n{func.__doc__}" return decorated_func diff --git a/src/boost_histogram/_internal/hist.py b/src/boost_histogram/_internal/hist.py index e1ff246dd..53b734fed 100644 --- a/src/boost_histogram/_internal/hist.py +++ b/src/boost_histogram/_internal/hist.py @@ -25,7 +25,7 @@ import numpy as np import boost_histogram -import boost_histogram._core as _core +from boost_histogram import _core from .axestuple import AxesTuple from .axis import Axis @@ -74,12 +74,14 @@ def _fill_cast( """ if value is None or isinstance(value, (str, bytes)): return value # type: ignore[return-value] - elif not inner and isinstance(value, (tuple, list)): + + if not inner and isinstance(value, (tuple, list)): return tuple(_fill_cast(a, inner=True) for a in value) # type: ignore[misc] - elif hasattr(value, "__iter__") or hasattr(value, "__array__"): + + if hasattr(value, "__iter__") or hasattr(value, "__array__"): return np.asarray(value) - else: - return value + + return value def _arg_shortcut(item: Union[Tuple[int, float, float], Axis, CppAxis]) -> CppAxis: @@ -87,10 +89,11 @@ def _arg_shortcut(item: Union[Tuple[int, float, float], Axis, CppAxis]) -> CppAx msg = "Developer shortcut: will be removed in a future version" warnings.warn(msg, FutureWarning) return _core.axis.regular_uoflow(item[0], item[1], item[2]) # type: ignore[return-value] - elif isinstance(item, Axis): + + if isinstance(item, Axis): return item._ax # type: ignore[no-any-return] - else: - raise TypeError("Only axes supported in histogram constructor") + + raise TypeError("Only axes supported in histogram constructor") def _expand_ellipsis(indexes: Iterable[Any], rank: int) -> List[Any]: @@ -98,7 +101,7 @@ def _expand_ellipsis(indexes: Iterable[Any], rank: int) -> List[Any]: number_ellipses = indexes.count(Ellipsis) if number_ellipses == 0: return indexes - elif number_ellipses == 1: + if number_ellipses == 1: index = indexes.index(Ellipsis) additional = rank + 1 - len(indexes) if additional < 0: @@ -107,8 +110,7 @@ def _expand_ellipsis(indexes: Iterable[Any], rank: int) -> List[Any]: # Fill out the ellipsis with empty slices return indexes[:index] + [slice(None)] * additional + indexes[index + 1 :] - else: - raise IndexError("an index can only have a single ellipsis ('...')") + raise IndexError("an index can only have a single ellipsis ('...')") H = TypeVar("H", bound="Histogram") @@ -192,13 +194,13 @@ def __init__( # support that too if len(axes) == 1 and isinstance(axes[0], Histogram): # Special case - we can recursively call __init__ here - self.__init__(axes[0]._hist) # type: ignore[misc] + self.__init__(axes[0]._hist) # type: ignore[misc] # pylint: disable=non-parent-init-called self._from_histogram_object(axes[0]) return # Support objects that provide a to_boost method, like Uproot - elif len(axes) == 1 and hasattr(axes[0], "_to_boost_histogram_"): - self.__init__(axes[0]._to_boost_histogram_()) # type: ignore[misc, union-attr, union-attr, union-attr] + if len(axes) == 1 and hasattr(axes[0], "_to_boost_histogram_"): + self.__init__(axes[0]._to_boost_histogram_()) # type: ignore[misc, union-attr, union-attr, union-attr] # pylint: disable=non-parent-init-called return if storage is None: @@ -208,20 +210,18 @@ def __init__( # Check for missed parenthesis or incorrect types if not isinstance(storage, Storage): - if issubclass(storage, Storage): - raise KeyError( - "Passing in an initialized storage has been removed. Please add ()." - ) - else: - raise KeyError("Only storages allowed in storage argument") + msg_storage = ( + "Passing in an initialized storage has been removed. Please add ()." + ) + msg_unknown = "Only storages allowed in storage argument" + raise KeyError(msg_storage if issubclass(storage, Storage) else msg_unknown) # Allow a tuple to represent a regular axis axes = tuple(_arg_shortcut(arg) for arg in axes) # type: ignore[arg-type] if len(axes) > _core.hist._axes_limit: - raise IndexError( - f"Too many axes, must be less than {_core.hist._axes_limit}" - ) + msg = f"Too many axes, must be less than {_core.hist._axes_limit}" + raise IndexError(msg) # Check all available histograms, and if the storage matches, return that one for h in _histograms: @@ -403,27 +403,24 @@ def _compute_inplace_op( getattr(self._hist, name)(other) elif hasattr(other, "shape") and other.shape: # type: ignore[union-attr] assert not isinstance(other, float) + if len(other.shape) != self.ndim: - raise ValueError( - "Number of dimensions {} must match histogram {}".format( - len(other.shape), self.ndim - ) - ) - elif all(a in {b, 1} for a, b in zip(other.shape, self.shape)): + msg = f"Number of dimensions {len(other.shape)} must match histogram {self.ndim}" + raise ValueError(msg) + + if all(a in {b, 1} for a, b in zip(other.shape, self.shape)): view = self.view(flow=False) getattr(view, name)(other) elif all(a in {b, 1} for a, b in zip(other.shape, self.axes.extent)): view = self.view(flow=True) getattr(view, name)(other) else: - raise ValueError( - "Wrong shape {}, expected {} or {}".format( - other.shape, self.shape, self.axes.extent - ) - ) + msg = f"Wrong shape {other.shape}, expected {self.shape} or {self.axes.extent}" + raise ValueError(msg) else: view = self.view(flow=True) getattr(view, name)(other) + self._variance_known = False return self @@ -601,20 +598,14 @@ def __setstate__(self, state: Any) -> None: def __repr__(self) -> str: newline = "\n " - sep = "," if len(self.axes) > 0 else "" - ret = "{self.__class__.__name__}({newline}".format( - self=self, newline=newline if len(self.axes) > 1 else "" + first_newline = newline if len(self.axes) > 1 else "" + storage_newline = ( + newline if len(self.axes) > 1 else " " if len(self.axes) > 0 else "" ) + sep = "," if len(self.axes) > 0 else "" + ret = f"{self.__class__.__name__}({first_newline}" ret += f",{newline}".join(repr(ax) for ax in self.axes) - ret += "{comma}{newline}storage={storage}".format( - storage=self._storage_type(), - newline=newline - if len(self.axes) > 1 - else " " - if len(self.axes) > 0 - else "", - comma=sep, - ) + ret += f"{sep}{storage_newline}storage={self._storage_type()}" # pylint: disable=not-callable ret += ")" outer = self.sum(flow=True) if outer: @@ -674,7 +665,8 @@ def _compute_commonindex( raise IndexError("Wrong number of indices for histogram") # Allow [bh.loc(...)] to work - for i in range(len(indexes)): + # TODO: could be nicer making a new list via a comprehension + for i in range(len(indexes)): # pylint: disable=consider-using-enumerate # Support list of UHI indexers if isinstance(indexes[i], list): indexes[i] = [self._compute_uhi_index(index, i) for index in indexes[i]] @@ -718,10 +710,7 @@ def to_numpy( hist, *edges = self._hist.to_numpy(flow) hist = self.view(flow=flow) if view else self.values(flow=flow) - if dd: - return (hist, edges) - else: - return (hist, *edges) + return (hist, edges) if dd else (hist, *edges) def copy(self: H, *, deep: bool = True) -> H: """ @@ -730,10 +719,7 @@ def copy(self: H, *, deep: bool = True) -> H: to avoid making a copy of axis metadata. """ - if deep: - return copy.deepcopy(self) - else: - return copy.copy(self) + return copy.deepcopy(self) if deep else copy.copy(self) def reset(self: H) -> H: """ @@ -786,8 +772,8 @@ def __getitem__( # noqa: C901 integrations: Set[int] = set() slices: List[_core.algorithm.reduce_command] = [] - pick_each: Dict[int, int] = dict() - pick_set: Dict[int, List[int]] = dict() + pick_each: Dict[int, int] = {} + pick_set: Dict[int, List[int]] = {} # Compute needed slices and projections for i, ind in enumerate(indexes): @@ -796,10 +782,12 @@ def __getitem__( # noqa: C901 1 if self.axes[i].traits.underflow else 0 ) continue - elif isinstance(ind, collections.abc.Sequence): + + if isinstance(ind, collections.abc.Sequence): pick_set[i] = list(ind) continue - elif not isinstance(ind, slice): + + if not isinstance(ind, slice): raise IndexError( "Must be a slice, an integer, or follow the locator protocol." ) @@ -875,7 +863,7 @@ def __getitem__( # noqa: C901 logger.debug("Slices for picking sets: %s", pick_set) axes = [reduced.axis(i) for i in range(reduced.rank())] reduced_view = reduced.view(flow=True) - for i in pick_set: + for i in pick_set: # pylint: disable=consider-using-dict-items selection = copy.copy(pick_set[i]) ax = reduced.axis(i) if ax.traits_ordered: @@ -952,11 +940,8 @@ def __setitem__( # to allow it (because we do allow broadcasting up dimensions) # Instead, we simply require matching dimensions. if value_ndim > 0 and value_ndim != sum(isinstance(i, slice) for i in indexes): - raise ValueError( - "Setting a {}D histogram with a {}D array must have a matching number of dimensions".format( - len(indexes), value_ndim - ) - ) + msg = f"Setting a {len(indexes)}D histogram with a {value_ndim}D array must have a matching number of dimensions" + raise ValueError(msg) # Here, value_n does not increment with n if this is not a slice value_n = 0 @@ -998,9 +983,7 @@ def __setitem__( msg = f"Mismatched shapes in dimension {n}" msg += f", {value_shape[n]} != {request_len}" if use_underflow or use_overflow: - msg += " or {}".format( - request_len + use_underflow + use_overflow - ) + msg += f" or {request_len + use_underflow + use_overflow}" raise ValueError(msg) indexes[n] = slice(start, stop, request.step) value_n += 1 @@ -1028,13 +1011,12 @@ def kind(self) -> Kind: :return: Kind """ - if self._hist._storage_type in { + mean = self._hist._storage_type in { _core.storage.mean, _core.storage.weighted_mean, - }: - return Kind.MEAN - else: - return Kind.COUNT + } + + return Kind.MEAN if mean else Kind.COUNT def values(self, flow: bool = False) -> "np.typing.NDArray[Any]": """ @@ -1054,8 +1036,7 @@ def values(self, flow: bool = False) -> "np.typing.NDArray[Any]": # TODO: Might be a NumPy typing bug if len(view.dtype) == 0: # type: ignore[arg-type] return view - else: - return view.value # type: ignore[union-attr] + return view.value # type: ignore[union-attr] def variances(self, flow: bool = False) -> Optional["np.typing.NDArray[Any]"]: """ @@ -1083,11 +1064,9 @@ def variances(self, flow: bool = False) -> Optional["np.typing.NDArray[Any]"]: view = self.view(flow) if len(view.dtype) == 0: # type: ignore[arg-type] - if self._variance_known: - return view - else: - return None - elif hasattr(view, "sum_of_weights"): + return view if self._variance_known else None + + if hasattr(view, "sum_of_weights"): return np.divide( # type: ignore[no-any-return] view.variance, # type: ignore[union-attr] view.sum_of_weights, # type: ignore[union-attr, union-attr, union-attr] @@ -1095,15 +1074,15 @@ def variances(self, flow: bool = False) -> Optional["np.typing.NDArray[Any]"]: where=view.sum_of_weights > 1, # type: ignore[union-attr, union-attr, union-attr] ) - elif hasattr(view, "count"): + if hasattr(view, "count"): return np.divide( # type: ignore[no-any-return] view.variance, # type: ignore[union-attr] view.count, # type: ignore[union-attr, union-attr, union-attr] out=np.full(view.count.shape, np.nan), # type: ignore[union-attr, union-attr, union-attr] where=view.count > 1, # type: ignore[union-attr, union-attr, union-attr] ) - else: - return view.variance # type: ignore[union-attr] + + return view.variance # type: ignore[union-attr] def counts(self, flow: bool = False) -> "np.typing.NDArray[Any]": """ @@ -1132,22 +1111,22 @@ def counts(self, flow: bool = False) -> "np.typing.NDArray[Any]": if len(view.dtype) == 0: # type: ignore[arg-type] return view - elif hasattr(view, "sum_of_weights"): + + if hasattr(view, "sum_of_weights"): return np.divide( # type: ignore[no-any-return] view.sum_of_weights ** 2, # type: ignore[union-attr, union-attr, union-attr] view.sum_of_weights_squared, # type: ignore[union-attr, union-attr, union-attr] out=np.zeros_like(view.sum_of_weights, dtype=np.float64), # type: ignore[union-attr, union-attr, union-attr] where=view.sum_of_weights_squared != 0, # type: ignore[union-attr, union-attr, union-attr] ) - elif hasattr(view, "count"): + + if hasattr(view, "count"): return view.count # type: ignore[union-attr, union-attr, union-attr] - else: - return view.value # type: ignore[union-attr] + return view.value # type: ignore[union-attr] -if TYPE_CHECKING: - import typing +if TYPE_CHECKING: from uhi.typing.plottable import PlottableHistogram _: PlottableHistogram = typing.cast(Histogram, None) diff --git a/src/boost_histogram/_internal/utils.py b/src/boost_histogram/_internal/utils.py index 86c44ec56..e7a2e2635 100644 --- a/src/boost_histogram/_internal/utils.py +++ b/src/boost_histogram/_internal/utils.py @@ -98,7 +98,7 @@ def _cast_make_object(canidate_class: T, cpp_object: object, is_class: bool) -> if is_class: return canidate_class - elif hasattr(canidate_class, "_convert_cpp"): + if hasattr(canidate_class, "_convert_cpp"): return canidate_class._convert_cpp(cpp_object) # type: ignore[attr-defined, no-any-return] # Casting down does not work in pybind11, @@ -106,8 +106,7 @@ def _cast_make_object(canidate_class: T, cpp_object: object, is_class: bool) -> # so for now, all non-copy classes must have a # _convert_cpp method. - else: - return canidate_class(cpp_object) # type: ignore[operator, no-any-return] + return canidate_class(cpp_object) # type: ignore[operator, no-any-return] def cast(self: object, cpp_object: object, parent_class: Type[T]) -> T: diff --git a/src/boost_histogram/_internal/view.py b/src/boost_histogram/_internal/view.py index 2277cbdf9..9490478ce 100644 --- a/src/boost_histogram/_internal/view.py +++ b/src/boost_histogram/_internal/view.py @@ -16,12 +16,13 @@ def __getitem__(self, ind: StrIndex) -> "np.typing.NDArray[Any]": # If the shape is empty, return the parent type if not sliced.shape: return self._PARENT._make(*sliced) # type: ignore[attr-defined, no-any-return] + # If the dtype has changed, return a normal array (no longer a record) - elif sliced.dtype != self.dtype: + if sliced.dtype != self.dtype: return np.asarray(sliced) + # Otherwise, no change, return the same View type - else: - return sliced # type: ignore[no-any-return] + return sliced # type: ignore[no-any-return] def __repr__(self) -> str: # NumPy starts the ndarray class name with "array", so we replace it @@ -29,10 +30,8 @@ def __repr__(self) -> str: return f"{self.__class__.__name__}(\n " + repr(self.view(np.ndarray))[6:] def __str__(self) -> str: - fields = ", ".join(self._FIELDS) - return "{self.__class__.__name__}: ({fields})\n{arr}".format( - self=self, fields=fields, arr=self.view(np.ndarray) - ) + my_fields = ", ".join(self._FIELDS) + return f"{self.__class__.__name__}: ({my_fields})\n{self.view(np.ndarray)}" def __setitem__(self, ind: StrIndex, value: ArrayLike) -> None: # `.value` really is ["value"] for an record array @@ -81,11 +80,11 @@ def injector(cls: Type[object]) -> Type[object]: raise RuntimeError( f"{cls.__name__} already has had a fields decorator applied" ) - fields = [] + my_fields = [] for name in names: - fields.append(name) + my_fields.append(name) setattr(cls, name, make_getitem_property(name)) - cls._FIELDS = tuple(fields) # type: ignore[attr-defined] + cls._FIELDS = tuple(my_fields) # type: ignore[attr-defined] return cls @@ -166,7 +165,7 @@ def __array_ufunc__( ) return result.view(self.__class__) # type: ignore[no-any-return] - elif ufunc in {np.multiply, np.divide, np.true_divide, np.floor_divide}: + if ufunc in {np.multiply, np.divide, np.true_divide, np.floor_divide}: if self.dtype == input_0.dtype: ufunc(input_0["value"], input_1, out=result["value"], **kwargs) ufunc( @@ -243,6 +242,5 @@ def _to_view( ret = item.view(cls) if value and ret.shape: return ret.value # type: ignore[no-any-return,attr-defined] - else: - return ret + return ret return item diff --git a/src/boost_histogram/accumulators.py b/src/boost_histogram/accumulators.py index f52f79e21..402f2f58c 100644 --- a/src/boost_histogram/accumulators.py +++ b/src/boost_histogram/accumulators.py @@ -1,4 +1,9 @@ -from ._core.accumulators import Mean, Sum, WeightedMean, WeightedSum +from ._core.accumulators import ( # pylint: disable=import-error + Mean, + Sum, + WeightedMean, + WeightedSum, +) from ._internal.typing import Accumulator __all__ = ("Sum", "Mean", "WeightedSum", "WeightedMean", "Accumulator") diff --git a/src/boost_histogram/numpy.py b/src/boost_histogram/numpy.py index 652778d32..586bc620f 100644 --- a/src/boost_histogram/numpy.py +++ b/src/boost_histogram/numpy.py @@ -1,15 +1,15 @@ -from functools import reduce as _reduce -from operator import mul as _mul -from typing import TYPE_CHECKING, Any, Optional, Sequence, Tuple, Type, Union +from functools import reduce +from operator import mul +from typing import TYPE_CHECKING, Any, List, Optional, Sequence, Tuple, Type, Union if TYPE_CHECKING: from numpy.typing import ArrayLike else: ArrayLike = object -import numpy as _np +import numpy as np -import boost_histogram._core as _core +from boost_histogram import _core from . import axis as _axis from . import storage as _storage @@ -19,19 +19,26 @@ __all__ = ("histogram", "histogram2d", "histogramdd") +def __dir__() -> List[str]: + return list(__all__) + + def histogramdd( a: Tuple[ArrayLike, ...], bins: Union[int, Tuple[int, ...]] = 10, - range: Optional[Sequence[Union[None, Tuple[float, float]]]] = None, + range: Optional[ # pylint: disable=redefined-builtin + Sequence[Union[None, Tuple[float, float]]] + ] = None, normed: None = None, weights: Optional[ArrayLike] = None, density: bool = False, *, - histogram: Optional[Type[_hist.Histogram]] = None, + histogram: Optional[ # pylint: disable=redefined-outer-name + Type[_hist.Histogram] + ] = None, storage: _storage.Storage = _storage.Double(), # noqa: B008 threads: Optional[int] = None ) -> Any: - np = _np # Hidden to keep module clean # TODO: Might be a bug in MyPy? This should type cls: Type[_hist.Histogram] = _hist.Histogram if histogram is None else histogram # type: ignore[assignment] @@ -79,7 +86,7 @@ def histogramdd( hist = cls(*axs, storage=storage).fill(*a, weight=weights, threads=threads) if density: - areas = _reduce(_mul, hist.axes.widths) + areas = reduce(mul, hist.axes.widths) density_val = hist.values() / np.sum(hist.values()) / areas return (density_val, hist.to_numpy()[1:]) @@ -93,12 +100,16 @@ def histogram2d( x: ArrayLike, y: ArrayLike, bins: Union[int, Tuple[int, int]] = 10, - range: Optional[Sequence[Union[None, Tuple[float, float]]]] = None, + range: Optional[ # pylint: disable=redefined-builtin + Sequence[Union[None, Tuple[float, float]]] + ] = None, normed: None = None, weights: Optional[ArrayLike] = None, density: bool = False, *, - histogram: Optional[Type[_hist.Histogram]] = None, + histogram: Optional[ # pylint: disable=redefined-outer-name + Type[_hist.Histogram] + ] = None, storage: _storage.Storage = _storage.Double(), # noqa: B008 threads: Optional[int] = None ) -> Any: @@ -124,16 +135,17 @@ def histogram2d( def histogram( a: ArrayLike, bins: int = 10, - range: Optional[Tuple[float, float]] = None, + range: Optional[Tuple[float, float]] = None, # pylint: disable=redefined-builtin normed: None = None, weights: Optional[ArrayLike] = None, density: bool = False, *, - histogram: Optional[Type[_hist.Histogram]] = None, + histogram: Optional[ # pylint: disable=redefined-outer-name + Type[_hist.Histogram] + ] = None, storage: Optional[_storage.Storage] = None, threads: Optional[int] = None ) -> Any: - np = _np # numpy 1d histogram returns integers in some cases if storage is None: @@ -170,9 +182,10 @@ def histogram( # Process docstrings -for f, n in zip( +# TODO: make this a decorator +for f, np_f in zip( (histogram, histogram2d, histogramdd), - (_np.histogram, _np.histogram2d, _np.histogramdd), + (np.histogram, np.histogram2d, np.histogramdd), ): H = """\ @@ -183,6 +196,4 @@ def histogram( lets you set the number of threads to fill with (0 for auto, None for 1). """ - f.__doc__ = H.format(n.__name__) + (n.__doc__ or "") - -del f, n, H + f.__doc__ = H.format(np_f.__name__) + (np_f.__doc__ or "") diff --git a/src/boost_histogram/tag.py b/src/boost_histogram/tag.py index 08a3ddabc..df4d3561b 100644 --- a/src/boost_histogram/tag.py +++ b/src/boost_histogram/tag.py @@ -1,5 +1,6 @@ # bh.sum is just the Python sum, so from boost_histogram import * is safe (but # not recommended) +import copy from builtins import sum from typing import TypeVar, Union @@ -37,20 +38,16 @@ def __init__(self, offset: int = 0) -> None: self.offset = offset def __add__(self: T, offset: int) -> T: - from copy import copy - - other = copy(self) + other = copy.copy(self) other.offset += offset return other def __sub__(self: T, offset: int) -> T: - from copy import copy - - other = copy(self) + other = copy.copy(self) other.offset -= offset return other - def _print_self_(self) -> str: + def _print_self_(self) -> str: # pylint: disable=no-self-use return "" def __repr__(self) -> str: