Skip to content

Linting and Formatting: adding Pylint checks to Ruff #1025

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

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6a9181b
setup PL checks
Laurynas-Jagutis Jun 18, 2025
7193e10
setup PL checks, update branch
Laurynas-Jagutis Jun 18, 2025
dd9c8ad
adjust pyproject
Laurynas-Jagutis Jun 18, 2025
8bd78aa
update ruff version
Laurynas-Jagutis Jun 18, 2025
b3085ea
Merge remote-tracking branch 'origin/main' into feature/add-PL-checks…
Laurynas-Jagutis Jul 7, 2025
fc79838
investigate why local and remote ruff differs
Laurynas-Jagutis Jul 7, 2025
b2b747f
remove ignore
Laurynas-Jagutis Jul 7, 2025
75e3740
fix issues
Laurynas-Jagutis Jul 7, 2025
8424854
add an example change for PLR2004
Laurynas-Jagutis Jul 7, 2025
e843b8a
change magic values to constants
Laurynas-Jagutis Jul 7, 2025
940009b
Merge remote-tracking branch 'origin/main' into feature/add-PL-checks…
Laurynas-Jagutis Jul 7, 2025
8edceef
change constant name
Laurynas-Jagutis Jul 8, 2025
64e73d9
add some ruff PL fixes to the tests
Laurynas-Jagutis Jul 8, 2025
3f87772
fix PLR5501 in tests
Laurynas-Jagutis Jul 8, 2025
b83d80f
Merge remote-tracking branch 'origin/main' into feature/add-PL-checks…
Laurynas-Jagutis Jul 8, 2025
dfc7248
fix .items() and change ignored checks in tests
Laurynas-Jagutis Jul 8, 2025
719014f
fix magic values1
Laurynas-Jagutis Jul 8, 2025
6e6f25d
fix magic values2
Laurynas-Jagutis Jul 8, 2025
73f3119
Merge remote-tracking branch 'origin/main' into feature/add-PL-checks…
Laurynas-Jagutis Jul 8, 2025
86cd924
adjust pyproject ignore setup
Laurynas-Jagutis Jul 8, 2025
9b2d1ab
Merge remote-tracking branch 'origin/main' into feature/add-PL-checks…
Laurynas-Jagutis Jul 14, 2025
11a236d
update name, capitalize module level constants
Laurynas-Jagutis Jul 14, 2025
467b493
capitalize module level constants
Laurynas-Jagutis Jul 14, 2025
819a42a
capitalize module level constants3
Laurynas-Jagutis Jul 14, 2025
693944d
capitalize module level constants4
Laurynas-Jagutis Jul 14, 2025
d57fedb
add constants for 1D
Laurynas-Jagutis Jul 14, 2025
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ repos:
- id: reuse
- repo: https://github.com/astral-sh/ruff-pre-commit
# Ruff version.
rev: v0.11.12
rev: v0.12.0
hooks:
# Run the linter.
- id: ruff-check
Expand Down
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ select = [
"FURB",
"FLY",
"SLOT",
"PL",
"NPY",
]

Expand All @@ -123,6 +124,9 @@ combine-as-imports = true
[tool.ruff.lint.per-file-ignores]
# Ignore `F811` (redefinition violations) in all examples notebooks since we use redefinition.
"docs/examples/*.ipynb" = ["F811", "E402"]
# Pylint was only run in src directory before moving to Ruff
"tests/*" = ["PLR0915", "PLR0912", "PLR0913"]
"setup.py" = ["PL"]

[tool.mypy]
follow_imports = "silent"
Expand Down
10 changes: 7 additions & 3 deletions src/power_grid_model/_core/buffer_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,8 @@ def _get_raw_attribute_data_view(data: np.ndarray, schema: ComponentMetaData, at
Returns:
a raw view on the data set.
"""
if schema.dtype[attribute].shape == (3,) and data.shape[-1] != 3:
supported_dim = 3
if schema.dtype[attribute].shape == (supported_dim,) and data.shape[-1] != supported_dim:
Comment on lines +120 to +121
Copy link
Member

Choose a reason for hiding this comment

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

This is both not descriptive enough and also not correct. This is exactly why you should be very careful with magic values. I myself did not understand what it was about until I dug more deeply into this.

  1. 3 is NOT a supported amount of dimensions, UNLESS a very specific case. Therefore, supported_dim is not a good name.
  2. Only 1-dimensional and 2-dimensional datastructures are supported. There is only 1 exception and that is when we're dealing with asymmetric values in dense batch attribute data, in which case you have 3 dimensions and the last dimension contains exactly 3 values
  3. This also means that if statement is incorrect (i.e. we found a bug)

Here's my proposal:

Suggested change
supported_dim = 3
if schema.dtype[attribute].shape == (supported_dim,) and data.shape[-1] != supported_dim:
dense_batch_ndim = 2
dense_batch_asym_attr_ndim = dense_batch_ndim + 1
n_asym_values = 3
shape = schema.dtype[attribute].shape
if len(shape) > batch_ndim and not (len(shape) == dense_batch_asym_attr_ndim and shape[-1] != n_asym_values):

you can also invert the if statement:

    dense_batch_ndim = 2
    dense_batch_asym_attr_ndim = dense_batch_ndim + 1
    n_asym_values = 3
    shape = schema.dtype[attribute].shape
    if len(shape) <= batch_ndim or (len(shape) == dense_batch_asym_attr_ndim and shape[-1] != n_asym_values):
        return _get_raw_data_view(data, dtype=schema.dtype[attribute].base)
    raise ValueError("Given data has a different schema than supported.")

Please do this fix in a separate PR off main together with a unit test that tries to trigger the issue from the lowest-level public function that calls this function (i.e., get_buffer_view).

I recommend first creating the unit tests (don't forget to commit when if fails correctly) and then applying the fix

raise ValueError("Given data has a different schema than supported.")
return _get_raw_data_view(data, dtype=schema.dtype[attribute].base)

Expand Down Expand Up @@ -185,8 +186,11 @@ def _get_dense_buffer_properties(
if actual_ndim not in (1, 2):
raise ValueError(f"Array can only be 1D or 2D. {VALIDATOR_MSG}")

actual_is_batch = actual_ndim == 2
actual_batch_size = shape[0] if actual_is_batch else 1
singular_ndim = 1
batch_ndim = 2

actual_is_batch = actual_ndim == batch_ndim
actual_batch_size = shape[0] if actual_is_batch else singular_ndim
n_elements_per_scenario = shape[-1]
n_total_elements = actual_batch_size * n_elements_per_scenario

Expand Down
2 changes: 1 addition & 1 deletion src/power_grid_model/_core/power_grid_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,7 @@ def destroy_dataset_mutable(self, dataset: MutableDatasetPtr) -> None: # type:
pass # pragma: no cover

@make_c_binding
def dataset_mutable_add_buffer( # type: ignore[empty-body]
def dataset_mutable_add_buffer( # type: ignore[empty-body] # noqa: PLR0913
self,
dataset: MutableDatasetPtr,
component: str,
Expand Down
14 changes: 7 additions & 7 deletions src/power_grid_model/_core/power_grid_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ def _handle_errors(self, continue_on_batch_error: bool, batch_size: int, decode_
decode_error=decode_error,
)

def _calculate_impl(
def _calculate_impl( # noqa: PLR0913
self,
calculation_type: CalculationType,
symmetric: bool,
Expand Down Expand Up @@ -300,7 +300,7 @@ def _calculate_impl(

return output_data

def _calculate_power_flow(
def _calculate_power_flow( # noqa: PLR0913
self,
*,
symmetric: bool = True,
Expand Down Expand Up @@ -337,7 +337,7 @@ def _calculate_power_flow(
experimental_features=experimental_features,
)

def _calculate_state_estimation(
def _calculate_state_estimation( # noqa: PLR0913
self,
*,
symmetric: bool = True,
Expand Down Expand Up @@ -372,7 +372,7 @@ def _calculate_state_estimation(
experimental_features=experimental_features,
)

def _calculate_short_circuit(
def _calculate_short_circuit( # noqa: PLR0913
self,
*,
calculation_method: CalculationMethod | str = CalculationMethod.iec60909,
Expand Down Expand Up @@ -406,7 +406,7 @@ def _calculate_short_circuit(
experimental_features=experimental_features,
)

def calculate_power_flow(
def calculate_power_flow( # noqa: PLR0913
self,
*,
symmetric: bool = True,
Expand Down Expand Up @@ -505,7 +505,7 @@ def calculate_power_flow(
tap_changing_strategy=tap_changing_strategy,
)

def calculate_state_estimation(
def calculate_state_estimation( # noqa: PLR0913
self,
*,
symmetric: bool = True,
Expand Down Expand Up @@ -599,7 +599,7 @@ def calculate_state_estimation(
decode_error=decode_error,
)

def calculate_short_circuit(
def calculate_short_circuit( # noqa: PLR0913
self,
*,
calculation_method: CalculationMethod | str = CalculationMethod.iec60909,
Expand Down
16 changes: 10 additions & 6 deletions src/power_grid_model/_core/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@
from power_grid_model._core.power_grid_meta import initialize_array, power_grid_meta_data
from power_grid_model._core.typing import ComponentAttributeMapping, _ComponentAttributeMappingDict

SINGULAR_NDIM = 1
Copy link
Member

Choose a reason for hiding this comment

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

not a fan of the name SINGULAR. Maybe something like SINGLE_DATASET_NDIM (and BATCH_DATASET_NDIM, resp.)?

BATCH_NDIM = 2
UNSUPPORTED_NDIM = 3
Copy link
Member

Choose a reason for hiding this comment

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

creating this on a global level doesn't make sense. In fact, all amount of dimensions above BATCH_NDIM (or below 0, for all that matters) are unsupported and it is very prone to bugs (as is shown in my other comment).



def is_nan(data) -> bool:
"""
Expand Down Expand Up @@ -166,17 +170,17 @@ def get_batch_size(
for attribute, array in batch_data.items():
if attribute in sym_attributes:
break
if array.ndim == 1:
if array.ndim == SINGULAR_NDIM:
raise TypeError("Incorrect dimension present in batch data.")
if array.ndim == 2:
if array.ndim == BATCH_NDIM:
return 1
return array.shape[0]
sym_array = next(iter(batch_data.values()))

sym_array = cast(DenseBatchArray | BatchColumn, sym_array)
if sym_array.ndim == 3:
if sym_array.ndim == UNSUPPORTED_NDIM:
Copy link
Member

Choose a reason for hiding this comment

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

this seems like a bug to me

Suggested change
if sym_array.ndim == UNSUPPORTED_NDIM:
if sym_array.ndim > BATCH_NDIM:

or (but please double-check whether ndim == 0 is supported, or we may just introduce a new bug)

Suggested change
if sym_array.ndim == UNSUPPORTED_NDIM:
if sym_array.ndim not in (SINGULAR_NDIM, BATCH_NDIM):

raise TypeError("Incorrect dimension present in batch data.")
if sym_array.ndim == 1:
if sym_array.ndim == SINGULAR_NDIM:
return 1
return sym_array.shape[0]

Expand Down Expand Up @@ -222,7 +226,7 @@ def _split_numpy_array_in_batches(
Returns:
A list with a single numpy structured array per batch
"""
if data.ndim == 1:
if data.ndim == SINGULAR_NDIM:
return [data]
if data.ndim in [2, 3]:
return [data[i, ...] for i in range(data.shape[0])]
Expand Down Expand Up @@ -325,7 +329,7 @@ def convert_dataset_to_python_dataset(data: Dataset) -> PythonDataset:
# It is batch dataset if it is 2D array or a indptr/data structure
is_batch: bool | None = None
for component, array in data.items():
is_dense_batch = isinstance(array, np.ndarray) and array.ndim == 2
is_dense_batch = isinstance(array, np.ndarray) and array.ndim == BATCH_NDIM
is_sparse_batch = isinstance(array, dict) and "indptr" in array and "data" in array
if is_batch is not None and is_batch != (is_dense_batch or is_sparse_batch):
raise ValueError(
Expand Down
2 changes: 1 addition & 1 deletion src/power_grid_model/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@ def self_test():
raise PowerGridError from e


def _make_test_case(
def _make_test_case( # noqa: PLR0913
*,
output_path: Path,
input_data: SingleDataset,
Expand Down
13 changes: 7 additions & 6 deletions src/power_grid_model/validation/_rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ def not_less_or_equal(val: np.ndarray, *ref: np.ndarray):
return none_match_comparison(data, component, field, not_less_or_equal, ref_value, NotLessOrEqualError)


def all_between(
def all_between( # noqa: PLR0913
data: SingleDataset,
component: ComponentType,
field: str,
Expand Down Expand Up @@ -281,7 +281,7 @@ def outside(val: np.ndarray, *ref: np.ndarray) -> np.ndarray:
)


def all_between_or_at(
def all_between_or_at( # noqa: PLR0913
data: SingleDataset,
component: ComponentType,
field: str,
Expand Down Expand Up @@ -331,7 +331,7 @@ def outside(val: np.ndarray, *ref: np.ndarray) -> np.ndarray:
)


def none_match_comparison(
def none_match_comparison( # noqa: PLR0913
data: SingleDataset,
component: ComponentType,
field: str,
Expand Down Expand Up @@ -554,7 +554,7 @@ def all_valid_enum_values(
return []


def all_valid_associated_enum_values(
def all_valid_associated_enum_values( # noqa: PLR0913
data: SingleDataset,
component: ComponentType,
field: str,
Expand Down Expand Up @@ -774,7 +774,7 @@ def all_finite(data: SingleDataset, exceptions: dict[ComponentType, list[str]] |

invalid = np.isinf(array[field])
if invalid.any():
ids = data[component]["id"][invalid].flatten().tolist()
ids = array["id"][invalid].flatten().tolist()
errors.append(InfinityError(component, field, ids))
return errors

Expand Down Expand Up @@ -841,7 +841,8 @@ def not_all_missing(data: SingleDataset, fields: list[str], component_type: Comp
fields: List of fields
component_type: component type to check
"""
if len(fields) < 2:
min_fields = 2
if len(fields) < min_fields:
raise ValueError(
"The fields parameter must contain at least 2 fields. Otherwise use the none_missing function."
)
Expand Down
4 changes: 2 additions & 2 deletions src/power_grid_model/validation/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ def _process_power_sigma_and_p_q_sigma(
power_sigma[mask] = np.nansum(q_sigma[mask], axis=asym_axes)


def validate_required_values(
def validate_required_values( # noqa: PLR0915
data: SingleDataset, calculation_type: CalculationType | None = None, symmetric: bool = True
) -> list[MissingValueError]:
"""
Expand Down Expand Up @@ -631,7 +631,7 @@ def validate_branch3(data: SingleDataset, component: ComponentType) -> list[Vali
return errors


def validate_three_winding_transformer(data: SingleDataset) -> list[ValidationError]:
def validate_three_winding_transformer(data: SingleDataset) -> list[ValidationError]: # noqa: PLR0915
errors = validate_branch3(data, ComponentType.three_winding_transformer)
errors += _all_greater_than_zero(data, ComponentType.three_winding_transformer, "u1")
errors += _all_greater_than_zero(data, ComponentType.three_winding_transformer, "u2")
Expand Down
18 changes: 15 additions & 3 deletions src/power_grid_model/validation/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

from power_grid_model import ComponentType

MIN_FIELDS = 2
MIN_COMPONENTS = 2


class ValidationError(ABC):
"""
Expand Down Expand Up @@ -54,6 +57,8 @@ class ValidationError(ABC):

_delimiter: str = " and "

__hash__ = None # type: ignore[assignment]

@property
def component_str(self) -> str:
"""
Expand Down Expand Up @@ -165,7 +170,7 @@ def __init__(self, component: ComponentType, fields: list[str], ids: list[int]):
self.field = sorted(fields)
self.ids = sorted(ids)

if len(self.field) < 2:
if len(self.field) < MIN_FIELDS:
raise ValueError(f"{type(self).__name__} expects at least two fields: {self.field}")


Expand All @@ -191,9 +196,9 @@ def __init__(self, fields: list[tuple[ComponentType, str]], ids: list[tuple[Comp
self.field = sorted(fields)
self.ids = sorted(ids)

if len(self.field) < 2:
if len(self.field) < MIN_FIELDS:
raise ValueError(f"{type(self).__name__} expects at least two fields: {self.field}")
if len(self.component) < 2:
if len(self.component) < MIN_COMPONENTS:
raise ValueError(f"{type(self).__name__} expects at least two components: {self.component}")


Expand Down Expand Up @@ -241,6 +246,7 @@ class InvalidValueError(SingleFieldValidationError):

_message = "Field {field} contains invalid values for {n} {objects}."
values: list
__hash__ = None # type: ignore[assignment]

def __init__(self, component: ComponentType, field: str, ids: list[int], values: list):
super().__init__(component, field, ids)
Expand All @@ -265,6 +271,7 @@ class InvalidEnumValueError(SingleFieldValidationError):

_message = "Field {field} contains invalid {enum} values for {n} {objects}."
enum: Type[Enum] | list[Type[Enum]]
__hash__ = None # type: ignore[assignment]

def __init__(self, component: ComponentType, field: str, ids: list[int], enum: Type[Enum] | list[Type[Enum]]):
super().__init__(component, field, ids)
Expand Down Expand Up @@ -318,6 +325,7 @@ class IdNotInDatasetError(SingleFieldValidationError):

_message = "ID does not exist in {ref_dataset} for {n} {objects}."
ref_dataset: str
__hash__ = None # type: ignore[assignment]

def __init__(self, component: ComponentType, ids: list[int], ref_dataset: str):
super().__init__(component=component, field="id", ids=ids)
Expand Down Expand Up @@ -345,6 +353,7 @@ class InvalidIdError(SingleFieldValidationError):

_message = "Field {field} does not contain a valid {ref_components} id for {n} {objects}. {filters}"
ref_components: list[ComponentType]
__hash__ = None # type: ignore[assignment]

def __init__(
self,
Expand Down Expand Up @@ -399,6 +408,8 @@ class ComparisonError(SingleFieldValidationError):

RefType = int | float | str | tuple[int | float | str, ...]

__hash__ = None # type: ignore[assignment]

def __init__(self, component: ComponentType, field: str, ids: list[int], ref_value: "ComparisonError.RefType"):
super().__init__(component, field, ids)
self.ref_value = ref_value
Expand Down Expand Up @@ -512,6 +523,7 @@ class InvalidAssociatedEnumValueError(MultiFieldValidationError):

_message = "The combination of fields {field} results in invalid {enum} values for {n} {objects}."
enum: Type[Enum] | list[Type[Enum]]
__hash__ = None # type: ignore[assignment]

def __init__(
self,
Expand Down
6 changes: 4 additions & 2 deletions src/power_grid_model/validation/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def _eval_field_expression(data: np.ndarray, expression: str) -> np.ndarray:
if len(fields) == 1:
return data[fields[0]]

assert len(fields) == 2
max_num_fields = 2
assert len(fields) == max_num_fields
zero_div = np.logical_or(np.equal(data[fields[1]], 0.0), np.logical_not(np.isfinite(data[fields[1]])))
if np.any(zero_div):
result = np.full_like(data[fields[0]], np.nan)
Expand Down Expand Up @@ -126,6 +127,7 @@ def _update_component_array_data(
Update the data in a numpy array, with another numpy array,
indexed on the "id" field and only non-NaN values are overwritten.
"""
batch_ndim = 2
if update_data.dtype.names is None:
raise ValueError("Invalid data format")

Expand All @@ -142,7 +144,7 @@ def _update_component_array_data(
nan = _nan_type(component, field, DatasetType.update)
mask = ~np.isnan(update_data[field]) if np.isnan(nan) else np.not_equal(update_data[field], nan)

if mask.ndim == 2:
if mask.ndim == batch_ndim:
for phase in range(mask.shape[1]):
# find indexers of to-be-updated object
sub_mask = mask[:, phase]
Expand Down
Loading
Loading