Skip to content

Commit

Permalink
Remove DeprecatedClassMeta in favor of getattr
Browse files Browse the repository at this point in the history
Fix kedro-org/kedro-starters#137

Signed-off-by: Juan Luis Cano Rodríguez <juan_luis_cano@mckinsey.com>
  • Loading branch information
deepyaman authored and astrojuanlu committed Jun 27, 2023
1 parent 4536caf commit f84096b
Show file tree
Hide file tree
Showing 8 changed files with 91 additions and 149 deletions.
20 changes: 15 additions & 5 deletions kedro/io/cached_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
from __future__ import annotations

import logging
import warnings
from typing import Any

from kedro.io.core import VERSIONED_FLAG_KEY, AbstractDataSet, Version
from kedro.io.memory_dataset import MemoryDataset
from kedro.utils import DeprecatedClassMeta

# https://github.com/pylint-dev/pylint/issues/4300#issuecomment-1043601901
CachedDataSet: AbstractDataSet


class CachedDataset(AbstractDataSet):
Expand Down Expand Up @@ -120,7 +123,14 @@ def __getstate__(self):
return self.__dict__


class CachedDataSet(metaclass=DeprecatedClassMeta):
# pylint: disable=missing-class-docstring, too-few-public-methods

_DeprecatedClassMeta__alias = CachedDataset
def __getattr__(name):
if name == "CachedDataSet":
alias = CachedDataset
warnings.warn(
f"{repr(name)} has been renamed to {repr(alias.__name__)}, "
f"and the alias will be removed in Kedro 0.19.0",
DeprecationWarning,
stacklevel=2,
)
return alias
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(name)}")
20 changes: 15 additions & 5 deletions kedro/io/lambda_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
"""
from __future__ import annotations

import warnings
from typing import Any, Callable

from kedro.io.core import AbstractDataSet, DatasetError
from kedro.utils import DeprecatedClassMeta

# https://github.com/pylint-dev/pylint/issues/4300#issuecomment-1043601901
LambdaDataSet: AbstractDataSet


class LambdaDataset(AbstractDataSet):
Expand Down Expand Up @@ -121,7 +124,14 @@ def __init__(
self.metadata = metadata


class LambdaDataSet(metaclass=DeprecatedClassMeta):
# pylint: disable=missing-class-docstring, too-few-public-methods

_DeprecatedClassMeta__alias = LambdaDataset
def __getattr__(name):
if name == "LambdaDataSet":
alias = LambdaDataset
warnings.warn(
f"{repr(name)} has been renamed to {repr(alias.__name__)}, "
f"and the alias will be removed in Kedro 0.19.0",
DeprecationWarning,
stacklevel=2,
)
return alias
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(name)}")
20 changes: 15 additions & 5 deletions kedro/io/memory_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@
from __future__ import annotations

import copy
import warnings
from typing import Any

from kedro.io.core import AbstractDataSet, DatasetError
from kedro.utils import DeprecatedClassMeta

_EMPTY = object()

# https://github.com/pylint-dev/pylint/issues/4300#issuecomment-1043601901
MemoryDataSet: AbstractDataSet


class MemoryDataset(AbstractDataSet):
"""``MemoryDataset`` loads and saves data from/to an in-memory
Expand Down Expand Up @@ -139,7 +142,14 @@ def _copy_with_mode(data: Any, copy_mode: str) -> Any:
return copied_data


class MemoryDataSet(metaclass=DeprecatedClassMeta):
# pylint: disable=missing-class-docstring, too-few-public-methods

_DeprecatedClassMeta__alias = MemoryDataset
def __getattr__(name):
if name == "MemoryDataSet":
alias = MemoryDataset
warnings.warn(
f"{repr(name)} has been renamed to {repr(alias.__name__)}, "
f"and the alias will be removed in Kedro 0.19.0",
DeprecationWarning,
stacklevel=2,
)
return alias
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(name)}")
36 changes: 23 additions & 13 deletions kedro/io/partitioned_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@
from __future__ import annotations

import operator
import warnings
from copy import deepcopy
from typing import Any, Callable
from urllib.parse import urlparse
from warnings import warn

from cachetools import Cache, cachedmethod

Expand All @@ -19,7 +19,7 @@
parse_dataset_definition,
)
from kedro.io.data_catalog import CREDENTIALS_KEY
from kedro.utils import DeprecatedClassMeta, load_obj
from kedro.utils import load_obj

DATASET_CREDENTIALS_KEY = "dataset_credentials"
CHECKPOINT_CREDENTIALS_KEY = "checkpoint_credentials"
Expand All @@ -31,6 +31,10 @@

S3_PROTOCOLS = ("s3", "s3a", "s3n")

# https://github.com/pylint-dev/pylint/issues/4300#issuecomment-1043601901
PartitionedDataSet: AbstractDataSet
IncrementalDataSet: AbstractDataSet


class PartitionedDataset(AbstractDataSet):
# pylint: disable=too-many-instance-attributes,protected-access
Expand Down Expand Up @@ -230,7 +234,7 @@ def __init__( # pylint: disable=too-many-arguments

self._filepath_arg = filepath_arg
if self._filepath_arg in self._dataset_config:
warn(
warnings.warn(
f"'{self._filepath_arg}' key must not be specified in the dataset "
f"definition as it will be overwritten by partition path"
)
Expand Down Expand Up @@ -338,12 +342,6 @@ def _release(self) -> None:
self._invalidate_caches()


class PartitionedDataSet(metaclass=DeprecatedClassMeta):
# pylint: disable=missing-class-docstring, too-few-public-methods

_DeprecatedClassMeta__alias = PartitionedDataset


class IncrementalDataset(PartitionedDataset):
"""``IncrementalDataset`` inherits from ``PartitionedDataset``, which loads
and saves partitioned file-like data using the underlying dataset
Expand Down Expand Up @@ -560,8 +558,20 @@ def confirm(self) -> None:
self._checkpoint.save(partition_ids[-1]) # checkpoint to last partition


class IncrementalDataSet(metaclass=DeprecatedClassMeta):
# pylint: disable=missing-class-docstring, too-few-public-methods
_DEPRECATED_ERROR_CLASSES = {
"PartitionedDataSet": PartitionedDataset,
"IncrementalDataSet": IncrementalDataset,
}


# pylint: disable=unused-private-member
__DeprecatedClassMeta__alias = IncrementalDataset
def __getattr__(name):
if name in _DEPRECATED_ERROR_CLASSES:
alias = _DEPRECATED_ERROR_CLASSES[name]
warnings.warn(
f"{repr(name)} has been renamed to {repr(alias.__name__)}, "
f"and the alias will be removed in Kedro 0.19.0",
DeprecationWarning,
stacklevel=2,
)
return alias
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(name)}")
20 changes: 15 additions & 5 deletions kedro/runner/parallel_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import os
import pickle
import sys
import warnings
from collections import Counter
from concurrent.futures import FIRST_COMPLETED, ProcessPoolExecutor, wait
from itertools import chain
Expand All @@ -27,11 +28,13 @@
from kedro.pipeline import Pipeline
from kedro.pipeline.node import Node
from kedro.runner.runner import AbstractRunner, run_node
from kedro.utils import DeprecatedClassMeta

# see https://github.com/python/cpython/blob/master/Lib/concurrent/futures/process.py#L114
_MAX_WINDOWS_WORKERS = 61

# https://github.com/pylint-dev/pylint/issues/4300#issuecomment-1043601901
_SharedMemoryDataSet: Any


class _SharedMemoryDataset:
"""``_SharedMemoryDataset`` is a wrapper class for a shared MemoryDataset in SyncManager.
Expand Down Expand Up @@ -70,10 +73,17 @@ def save(self, data: Any):
raise exc


class _SharedMemoryDataSet(metaclass=DeprecatedClassMeta):
# pylint: disable=too-few-public-methods

_DeprecatedClassMeta__alias = _SharedMemoryDataset
def __getattr__(name):
if name == "_SharedMemoryDataSet":
alias = _SharedMemoryDataset
warnings.warn(
f"{repr(name)} has been renamed to {repr(alias.__name__)}, "
f"and the alias will be removed in Kedro 0.19.0",
DeprecationWarning,
stacklevel=2,
)
return alias
raise AttributeError(f"module {repr(__name__)} has no attribute {repr(name)}")


class ParallelRunnerManager(SyncManager):
Expand Down
65 changes: 0 additions & 65 deletions kedro/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""
import importlib
from typing import Any
from warnings import warn


def load_obj(obj_path: str, default_obj_path: str = "") -> Any:
Expand All @@ -27,67 +26,3 @@ def load_obj(obj_path: str, default_obj_path: str = "") -> Any:
if not hasattr(module_obj, obj_name):
raise AttributeError(f"Object '{obj_name}' cannot be loaded from '{obj_path}'.")
return getattr(module_obj, obj_name)


class DeprecatedClassMeta(type):
"""Metaclass for constructing deprecated aliases of renamed classes.
Code implementation copied from https://stackoverflow.com/a/52087847
"""

def __new__(mcs, name, bases, classdict, *args, **kwargs):
alias = classdict.get("_DeprecatedClassMeta__alias")

if alias is not None:

def new(mcs, *args, **kwargs):
alias = getattr(mcs, "_DeprecatedClassMeta__alias")

if alias is not None:
warn(
f"{repr(mcs.__name__)} has been renamed to "
f"{repr(alias.__name__)}, and the alias will "
f"be removed in Kedro 0.19.0",
DeprecationWarning,
stacklevel=2,
)

return alias(*args, **kwargs)

classdict["__new__"] = new
classdict["_DeprecatedClassMeta__alias"] = alias

fixed_bases = []

for base in bases:
alias = getattr(base, "_DeprecatedClassMeta__alias", None)

if alias is not None:
warn(
f"{repr(base.__name__)} has been renamed to "
f"{repr(alias.__name__)}, and the alias will be "
f"removed in Kedro 0.19.0",
DeprecationWarning,
stacklevel=2,
)

# Avoid duplicate base classes.
base = alias or base
if base not in fixed_bases:
fixed_bases.append(base)

fixed_bases = tuple(fixed_bases)

return super().__new__(mcs, name, fixed_bases, classdict, *args, **kwargs)

def __instancecheck__(cls, instance):
return any(
# pylint: disable=no-value-for-parameter
cls.__subclasscheck__(c)
for c in [type(instance), instance.__class__]
)

def __subclasscheck__(cls, subclass):
return subclass is cls or issubclass(
subclass, getattr(cls, "_DeprecatedClassMeta__alias")
)
7 changes: 7 additions & 0 deletions tests/runner/test_parallel_runner.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import importlib
import sys
from concurrent.futures.process import ProcessPoolExecutor
from typing import Any
Expand Down Expand Up @@ -33,6 +34,12 @@
)


def test_deprecation():
class_name = "_SharedMemoryDataSet"
with pytest.warns(DeprecationWarning, match=f"{repr(class_name)} has been renamed"):
getattr(importlib.import_module("kedro.runner.parallel_runner"), class_name)


@pytest.mark.skipif(
sys.platform.startswith("win"), reason="Due to bug in parallel runner"
)
Expand Down
52 changes: 1 addition & 51 deletions tests/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from kedro.utils import DeprecatedClassMeta, load_obj
from kedro.utils import load_obj


# pylint: disable=too-few-public-methods
Expand All @@ -28,53 +28,3 @@ def test_load_obj_invalid_attribute(self):
def test_load_obj_invalid_module(self):
with pytest.raises(ImportError, match=r"No module named 'missing_path'"):
load_obj("InvalidClass", "missing_path")


class NewClass:
value = 1


class NewClassSubclass(NewClass):
pass


class DeprecatedClass(metaclass=DeprecatedClassMeta):
_DeprecatedClassMeta__alias = NewClass


class DeprecatedClassSubclass(DeprecatedClass):
value = 2


class DeprecatedClassSubSubclass(DeprecatedClassSubclass):
value = 3


class TestDeprecatedClassMeta:
def test_is_subclass_of_deprecated_class(self):
assert issubclass(DeprecatedClass, DeprecatedClass)
assert issubclass(DeprecatedClassSubclass, DeprecatedClass)
assert issubclass(DeprecatedClassSubSubclass, DeprecatedClass)
assert issubclass(NewClass, DeprecatedClass)
assert issubclass(NewClassSubclass, DeprecatedClass)

def test_is_subclass_of_new_class(self):
assert issubclass(DeprecatedClassSubclass, NewClass)
assert issubclass(DeprecatedClassSubSubclass, NewClass)

def test_is_instance_of_deprecated_class(self):
assert isinstance(DeprecatedClass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubclass(), DeprecatedClass)
assert isinstance(DeprecatedClassSubSubclass(), DeprecatedClass)
assert isinstance(NewClass(), DeprecatedClass)
assert isinstance(NewClassSubclass(), DeprecatedClass)

def test_is_instance_of_new_class(self):
assert isinstance(DeprecatedClassSubclass(), NewClass)
assert isinstance(DeprecatedClassSubSubclass(), NewClass)

def test_inheritance(self):
assert NewClass().value == 1
assert DeprecatedClass().value == 1 # pylint: disable=no-member
assert DeprecatedClassSubclass().value == 2
assert DeprecatedClassSubSubclass().value == 3

0 comments on commit f84096b

Please sign in to comment.