diff --git a/scrapy/_classutilities.py b/scrapy/_classutilities.py new file mode 100644 index 000000000..ee58ea62f --- /dev/null +++ b/scrapy/_classutilities.py @@ -0,0 +1,94 @@ +# https://github.com/david-salac/classutilities/issues/1 +# Unmodified copy of +# https://github.com/david-salac/classutilities/blob/a6e4a86331936d432afaa454ed4c963528165a61/src/classutilities/classproperty.py + +# Allows creating a class level property +from typing import Any, Callable + + +class ClassPropertyContainer: + """ + Allows creating a class level property (functionality for + decorator). + """ + + def __init__(self, prop_get: Any, prop_set: Any = None): + """ + Container that allows having a class property decorator. + :param prop_get: Class property getter. + :param prop_set: Class property setter. + """ + self.prop_get: Any = prop_get + self.prop_set: Any = prop_set + + def __get__(self, obj: Any, cls: type = None) -> Callable: + """ + Get the property getter. + :param obj: Instance of the class. + :param cls: Type of the class. + :return: Class property getter. + """ + if cls is None: + cls = type(obj) + return self.prop_get.__get__(obj, cls)() + + def __set__(self, obj, value) -> Callable: + """ + Get the property setter. + :param obj: Instance of the class. + :param value: A value to be set. + :return: Class property setter. + """ + if not self.prop_set: + raise AttributeError("cannot set attribute") + _type: type = type(obj) + if _type == ClassPropertyMetaClass: + _type = obj + return self.prop_set.__get__(obj, _type)(value) + + def setter(self, func: Callable) -> "ClassPropertyContainer": + """ + Allows creating setter in a property like way. + :param func: Getter function. + :return: Setter object for the decorator. + """ + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + self.prop_set = func + return self + + +def classproperty(func): + """ + Create a decorator for a class level property. + :param func: This class method is decorated. + :return: Modified class method behaving like a class property. + """ + if not isinstance(func, (classmethod, staticmethod)): + # The method must be a classmethod (or staticmethod) + func = classmethod(func) + return ClassPropertyContainer(func) + + +class ClassPropertyMetaClass(type): + """ + Metaclass that allows creating a standard setter. + """ + + def __setattr__(self, key, value): + """Overloads setter for class""" + if key in self.__dict__: + obj = self.__dict__.get(key) + if obj and type(obj) is ClassPropertyContainer: + return obj.__set__(self, value) + + return super().__setattr__(key, value) + + +class ClassPropertiesMixin(metaclass=ClassPropertyMetaClass): + """ + This mixin allows using class properties setter (getter works + correctly even without this mixin) + """ + + pass diff --git a/scrapy/core/scraper.py b/scrapy/core/scraper.py index 6a350c236..02ab8d3e4 100644 --- a/scrapy/core/scraper.py +++ b/scrapy/core/scraper.py @@ -24,12 +24,12 @@ from typing import ( ) from warnings import warn -from classutilities import ClassPropertiesMixin, classproperty from itemadapter import is_item from twisted.internet.defer import Deferred, inlineCallbacks from twisted.python.failure import Failure from scrapy import Spider, signals +from scrapy._classutilities import ClassPropertiesMixin, classproperty from scrapy.core.spidermw import SpiderMiddlewareManager from scrapy.exceptions import ( CloseSpider, diff --git a/setup.py b/setup.py index 28a1dab89..2d6d26b0c 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,6 @@ version = (Path(__file__).parent / "scrapy/VERSION").read_text("ascii").strip() install_requires = [ "Twisted>=18.9.0", - "classutilities>=0.2.1", "cryptography>=36.0.0", "cssselect>=0.9.1", "itemloaders>=1.0.1", diff --git a/tox.ini b/tox.ini index f85c58f04..c325064d9 100644 --- a/tox.ini +++ b/tox.ini @@ -97,7 +97,6 @@ commands = [pinned] basepython = python3.8 deps = - classutilities==0.2.1 cryptography==36.0.0 cssselect==0.9.1 h2==3.0