diff --git a/pypdf/_utils.py b/pypdf/_utils.py index ed830d1d7..5560702c8 100644 --- a/pypdf/_utils.py +++ b/pypdf/_utils.py @@ -630,3 +630,54 @@ def __lt__(self, other: Any) -> bool: return False return len(self.components) < len(other.components) + + +# The following class has been copied from Django: +# https://github.com/django/django/blob/adae619426b6f50046b3daaa744db52989c9d6db/django/utils/functional.py#L51-L65 +# +# Original license: +# +# --------------------------------------------------------------------------------- +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of Django nor the names of its contributors may be used +# to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# --------------------------------------------------------------------------------- +class classproperty: + """ + Decorator that converts a method with a single cls argument into a property + that can be accessed directly from the class. + """ + + def __init__(self, method=None): + self.fget = method + + def __get__(self, instance, cls=None): + return self.fget(cls) + + def getter(self, method): + self.fget = method + return self diff --git a/pypdf/constants.py b/pypdf/constants.py index aed0b5d8b..689ce1834 100644 --- a/pypdf/constants.py +++ b/pypdf/constants.py @@ -14,7 +14,7 @@ from enum import IntFlag, auto from typing import Dict, Tuple -from ._utils import deprecate_with_replacement +from ._utils import classproperty, deprecate_with_replacement class Core: @@ -161,50 +161,42 @@ class Ressources: # deprecated .. deprecated:: 5.0.0 """ - @classmethod # type: ignore - @property + @classproperty def EXT_G_STATE(cls) -> str: deprecate_with_replacement("Ressources", "Resources", "5.0.0") return "/ExtGState" # dictionary, optional - @classmethod # type: ignore - @property + @classproperty def COLOR_SPACE(cls) -> str: deprecate_with_replacement("Ressources", "Resources", "5.0.0") return "/ColorSpace" # dictionary, optional - @classmethod # type: ignore - @property + @classproperty def PATTERN(cls) -> str: deprecate_with_replacement("Ressources", "Resources", "5.0.0") return "/Pattern" # dictionary, optional - @classmethod # type: ignore - @property + @classproperty def SHADING(cls) -> str: deprecate_with_replacement("Ressources", "Resources", "5.0.0") return "/Shading" # dictionary, optional - @classmethod # type: ignore - @property + @classproperty def XOBJECT(cls) -> str: deprecate_with_replacement("Ressources", "Resources", "5.0.0") return "/XObject" # dictionary, optional - @classmethod # type: ignore - @property + @classproperty def FONT(cls) -> str: deprecate_with_replacement("Ressources", "Resources", "5.0.0") return "/Font" # dictionary, optional - @classmethod # type: ignore - @property + @classproperty def PROC_SET(cls) -> str: deprecate_with_replacement("Ressources", "Resources", "5.0.0") return "/ProcSet" # array, optional - @classmethod # type: ignore - @property + @classproperty def PROPERTIES(cls) -> str: deprecate_with_replacement("Ressources", "Resources", "5.0.0") return "/Properties" # dictionary, optional diff --git a/tests/test_utils.py b/tests/test_utils.py index 508cfd2a4..8cddc6f01 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -11,6 +11,7 @@ _get_max_pdf_version_header, _human_readable_bytes, check_if_whitespace_only, + classproperty, deprecate_with_replacement, deprecation_no_replacement, mark_location, @@ -424,3 +425,19 @@ def test_version_compare_lt_str(): def test_bad_version(): assert Version("a").components == [(0, "a")] + + +def test_classproperty(): + class Container: + @classproperty + def value1(cls): + return 42 + + @classproperty + def value2(cls): + return 1337 + + assert Container.value1 == 42 + assert Container.value2 == 1337 + assert Container().value1 == 42 + assert Container().value2 == 1337