diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..90e05c4 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" diff --git a/.github/workflows/pythonpackage.yml b/.github/workflows/pythonpackage.yml index 7bd64af..25fcebd 100644 --- a/.github/workflows/pythonpackage.yml +++ b/.github/workflows/pythonpackage.yml @@ -6,9 +6,9 @@ jobs: PEP8: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4.2.2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5.3.0 with: python-version: '3.x' - name: Install dependencies @@ -22,11 +22,11 @@ jobs: PyLint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4.2.2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5.3.0 with: - python-version: '3.8' + python-version: '3.7' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -42,9 +42,9 @@ jobs: MyPy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4.2.2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5.3.0 with: python-version: '3.x' - name: Install dependencies @@ -62,9 +62,9 @@ jobs: Black: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4.2.2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5.3.0 with: python-version: '3.x' - name: Install dependencies @@ -81,13 +81,13 @@ jobs: strategy: max-parallel: 6 matrix: - os: [ubuntu-latest, windows-latest] - python-version: [3.7, 3.8, 3.9] + os: ["ubuntu-latest", "windows-latest"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4.2.2 - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 + uses: actions/setup-python@v5.3.0 with: python-version: ${{ matrix.python-version }} - name: Install dependencies @@ -97,7 +97,7 @@ jobs: pip install --upgrade -r pytest_requirements.txt - name: Build package and install develop run: | - python setup.py develop -v clean + pip install -e . - name: Test with pytest run: | py.test --cov-report= --cov=advanced_descriptors test @@ -108,19 +108,18 @@ jobs: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4.2.2 - name: Set up Python - uses: actions/setup-python@v2 + uses: actions/setup-python@v5.3.0 with: python-version: '3.x' - name: Install dependencies run: | - python -m pip install --upgrade pip wheel - pip install --upgrade -r CI_REQUIREMENTS.txt - pip install --upgrade twine + python -m pip install --upgrade pip + pip install --upgrade twine build - name: Build package run: | - python setup.py sdist bdist_wheel clean + python -m build - name: Deploy env: TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..7045420 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,45 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.3.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + + - repo: https://github.com/pycqa/isort + rev: 5.11.2 + hooks: + - id: isort + name: isort (python) + - id: isort + name: isort (cython) + types: [cython] + - id: isort + name: isort (pyi) + types: [pyi] + + - repo: https://github.com/psf/black + rev: 22.12.0 + hooks: + - id: black + # It is recommended to specify the latest version of Python + # supported by your project here, or alternatively use + # pre-commit's default_language_version, see + # https://pre-commit.com/#top_level-default_language_version + language_version: python3.9 + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 # Use the ref you want to point at + hooks: + - id: python-check-blanket-noqa + - id: python-check-blanket-type-ignore + - id: rst-directive-colons + - id: rst-inline-touching-normal + + - repo: https://github.com/PyCQA/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + additional_dependencies: [flake8-bugbear, pep8-naming, flake8-docstrings] + exclude: test diff --git a/.pylintrc b/.pylintrc index 02d5272..4a50091 100644 --- a/.pylintrc +++ b/.pylintrc @@ -7,11 +7,11 @@ extension-pkg-whitelist=lxml.etree # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=CVS,.git +ignore=CVS,.git,_version.py # Add files or directories matching the regex patterns to the blacklist. The # regex matches against base names, not paths. -ignore-patterns=_version.py +ignore-patterns= # Python code to execute, usually for sys.path manipulation such as # pygtk.require(). @@ -28,12 +28,13 @@ limit-inference-results=100 # List of plugins (as comma separated values of python modules names) to load, # usually to register additional checkers. -load-plugins=pylint.extensions.docparams, - pylint.extensions.docstyle, +load-plugins=pylint.extensions.docstyle, + pylint.extensions.docparams, pylint.extensions.overlapping_exceptions, pylint.extensions.emptystring, pylint.extensions.comparetozero, pylint.extensions.check_elif, + pylint.extensions.for_any_all, pylint.extensions.code_style, pylint.extensions.redefined_variable_type, pylint.extensions.typing, @@ -85,7 +86,6 @@ disable=locally-disabled, too-many-statements, too-many-instance-attributes, too-many-lines, - misplaced-comparison-constant, broad-except, logging-fstring-interpolation, logging-format-interpolation @@ -299,13 +299,6 @@ max-line-length=120 # Maximum number of lines in a module. max-module-lines=1000 -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - # Allow the body of a class to be on the same line as the declaration if body # contains single statement. single-line-class-stmt=no diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..d09df1f --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,27 @@ +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3" + # You can also specify other tool versions: + # nodejs: "16" + # rust: "1.55" + # golang: "1.17" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: doc/source/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +# formats: +# - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - method: pip + path: . + - requirements: CI_REQUIREMENTS.txt diff --git a/CI_REQUIREMENTS.txt b/CI_REQUIREMENTS.txt index bc04b49..eef06bf 100644 --- a/CI_REQUIREMENTS.txt +++ b/CI_REQUIREMENTS.txt @@ -1 +1,2 @@ -r requirements.txt +setuptools_scm diff --git a/LICENSE b/LICENSE index e81423f..6715d31 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2017-2020 Alexey Stepanov aka penguinolog + Copyright 2016-2022 Alexey Stepanov aka penguinolog Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/MANIFEST.in b/MANIFEST.in index e3e7d46..e1b881d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,4 @@ -include *.rst LICENSE requirements.txt +include *.rst LICENSE requirements.txt classifiers.txt global-include *.pyx *.pxd global-exclude *.c exclude Makefile @@ -9,5 +9,6 @@ exclude .gitignore .dockerignore prune test prune .github prune .azure_pipelines -prune docs +prune .tox +prune doc exclude CODEOWNERS CODE_OF_CONDUCT.md _config.yml diff --git a/README.rst b/README.rst index 985d7be..3e7cdc7 100644 --- a/README.rst +++ b/README.rst @@ -1,12 +1,8 @@ Advanced descriptors ==================== -.. image:: https://travis-ci.com/python-useful-helpers/advanced-descriptors.svg?branch=master - :target: https://travis-ci.com/python-useful-helpers/advanced-descriptors .. image:: https://github.com/python-useful-helpers/advanced-descriptors/workflows/Python%20package/badge.svg :target: https://github.com/python-useful-helpers/advanced-descriptors/actions -.. image:: https://coveralls.io/repos/github/python-useful-helpers/advanced-descriptors/badge.svg?branch=master - :target: https://coveralls.io/github/python-useful-helpers/advanced-descriptors?branch=master .. image:: https://readthedocs.org/projects/advanced-descriptors/badge/?version=latest :target: http://advanced-descriptors.readthedocs.io/ :alt: Documentation Status @@ -206,13 +202,6 @@ Available environments can be collected via `tox -l` CI systems ========== -For code checking several CI systems is used in parallel: +For CI/CD GitHub actions is used: -1. `Travis CI: `_ is used for checking: PEP8, pylint, bandit, installation possibility and unit tests. Also it's publishes coverage on coveralls. - -2. `GitHub actions: `_ is used for checking: PEP8, pylint, bandit, installation possibility and unit tests. -3. `coveralls: `_ is used for coverage display. - -CD system -========= -`Travis CI: `_ is used for package delivery on PyPI. +`GitHub actions: `_ is used for checking: PEP8, pylint, bandit, installation possibility and unit tests. diff --git a/advanced_descriptors/__init__.py b/advanced_descriptors/__init__.py index 370a184..7b430fa 100644 --- a/advanced_descriptors/__init__.py +++ b/advanced_descriptors/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2017 - 2021 Alexey Stepanov aka penguinolog +# Copyright 2017 - 2022 Alexey Stepanov aka penguinolog # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at diff --git a/advanced_descriptors/advanced_property.py b/advanced_descriptors/advanced_property.py index baed6f8..9719c27 100755 --- a/advanced_descriptors/advanced_property.py +++ b/advanced_descriptors/advanced_property.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2016 - 2021 Alexey Stepanov aka penguinolog +# Copyright 2016 - 2022 Alexey Stepanov aka penguinolog # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -20,6 +20,10 @@ # Standard Library import typing +if typing.TYPE_CHECKING: + # Standard Library + from collections.abc import Callable + __all__ = ("AdvancedProperty",) _OwnerClassT = typing.TypeVar("_OwnerClassT") @@ -121,45 +125,45 @@ class AdvancedProperty(property, typing.Generic[_OwnerClassT, _ReturnT, _ClassRe def __init__( self, - fget: typing.Optional[typing.Callable[[_OwnerClassT], _ReturnT]] = None, - fset: typing.Optional[typing.Callable[[_OwnerClassT, _ReturnT], None]] = None, - fdel: typing.Optional[typing.Callable[[_OwnerClassT], None]] = None, - fcget: typing.Optional[typing.Callable[[typing.Type[_OwnerClassT]], _ClassReturnT]] = None, + fget: Callable[[_OwnerClassT], _ReturnT] | None = None, + fset: Callable[[_OwnerClassT, _ReturnT], None] | None = None, + fdel: Callable[[_OwnerClassT], None] | None = None, + fcget: Callable[[type[_OwnerClassT]], _ClassReturnT] | None = None, ) -> None: """Advanced property main entry point. :param fget: normal getter. - :type fget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]] + :type fget: Callable[[typing.Any, ], typing.Any] | None :param fset: normal setter. - :type fset: typing.Optional[typing.Callable[[typing.Any, typing.Any], None]] + :type fset: Callable[[typing.Any, typing.Any], None] | None :param fdel: normal deleter. - :type fdel: typing.Optional[typing.Callable[[typing.Any, ], None]] + :type fdel: Callable[[typing.Any, ], None] | None :param fcget: class getter. Used as normal, if normal is None. - :type fcget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]] + :type fcget: Callable[[typing.Any, ], typing.Any] | None .. note:: doc argument is not supported due to class wide getter usage. """ super().__init__(fget=fget, fset=fset, fdel=fdel) - self.__fcget: typing.Optional[typing.Callable[[typing.Type[_OwnerClassT]], _ClassReturnT]] = fcget + self.__fcget: Callable[[type[_OwnerClassT]], _ClassReturnT] | None = fcget @typing.overload - def __get__(self, instance: None, owner: typing.Type[_OwnerClassT]) -> _ClassReturnT: + def __get__(self, instance: None, owner: type[_OwnerClassT]) -> _ClassReturnT: """Class method.""" @typing.overload - def __get__(self, instance: _OwnerClassT, owner: typing.Optional[typing.Type[_OwnerClassT]] = None) -> _ReturnT: + def __get__(self, instance: _OwnerClassT, owner: type[_OwnerClassT] | None = None) -> _ReturnT: """Normal method.""" def __get__( self, - instance: typing.Optional[_OwnerClassT], - owner: typing.Optional[typing.Type[_OwnerClassT]] = None, - ) -> typing.Union[_ClassReturnT, _ReturnT]: + instance: _OwnerClassT | None, + owner: type[_OwnerClassT] | None = None, + ) -> _ClassReturnT | _ReturnT: """Get descriptor. :param instance: Owner class instance. Filled only if instance created, else None. - :type instance: typing.Optional[owner] + :type instance: owner | None :param owner: Owner class for property. :return: getter call result if getter presents :rtype: typing.Any @@ -169,25 +173,25 @@ def __get__( if self.__fcget is None: raise AttributeError() return self.__fcget(owner) - return super().__get__(instance, owner) # type: ignore + return super().__get__(instance, owner) # type: ignore[no-any-return] @property - def fcget(self) -> typing.Optional[typing.Callable[[typing.Type[_OwnerClassT]], _ClassReturnT]]: + def fcget(self) -> Callable[[type[_OwnerClassT]], _ClassReturnT] | None: """Class wide getter instance. :return: Class wide getter instance - :rtype: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]] + :rtype: Callable[[typing.Any, ], typing.Any] | None """ return self.__fcget def cgetter( self, - fcget: typing.Optional[typing.Callable[[typing.Type[_OwnerClassT]], _ClassReturnT]], + fcget: Callable[[type[_OwnerClassT]], _ClassReturnT] | None, ) -> AdvancedProperty[_OwnerClassT, _ReturnT, _ClassReturnT]: """Descriptor to change the class wide getter on a property. :param fcget: new class-wide getter. - :type fcget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]] + :type fcget: Callable[[typing.Any, ], typing.Any] | None :return: AdvancedProperty :rtype: AdvancedProperty """ diff --git a/advanced_descriptors/log_on_access.py b/advanced_descriptors/log_on_access.py index f8cc267..b74074d 100644 --- a/advanced_descriptors/log_on_access.py +++ b/advanced_descriptors/log_on_access.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2016 - 2021 Alexey Stepanov aka penguinolog +# Copyright 2016 - 2022 Alexey Stepanov aka penguinolog # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -15,6 +15,8 @@ """Property with logging on successful get/set/delete or failure.""" +from __future__ import annotations + __all__ = ("LogOnAccess",) # Standard Library @@ -26,6 +28,10 @@ import typing import warnings +if typing.TYPE_CHECKING: + # Standard Library + from collections.abc import Callable + _LOGGER: logging.Logger = logging.getLogger(__name__) _CURRENT_FILE = os.path.abspath(__file__) _OwnerT = typing.TypeVar("_OwnerT") @@ -43,33 +49,33 @@ class LogOnAccess(property, typing.Generic[_OwnerT, _ReturnT]): def __init__( self, - fget: typing.Optional[typing.Callable[[_OwnerT], _ReturnT]] = None, - fset: typing.Optional[typing.Callable[[_OwnerT, _ReturnT], None]] = None, - fdel: typing.Optional[typing.Callable[[_OwnerT], None]] = None, - doc: typing.Optional[str] = None, + fget: Callable[[_OwnerT], _ReturnT] | None = None, + fset: Callable[[_OwnerT, _ReturnT], None] | None = None, + fdel: Callable[[_OwnerT], None] | None = None, + doc: str | None = None, *, # Extended settings start - logger: typing.Optional[typing.Union[logging.Logger, str]] = None, + logger: logging.Logger | str | None = None, log_object_repr: bool = True, log_level: int = logging.DEBUG, exc_level: int = logging.DEBUG, log_success: bool = True, log_failure: bool = True, log_traceback: bool = True, - override_name: typing.Optional[str] = None, + override_name: str | None = None, ) -> None: """Advanced property main entry point. :param fget: normal getter. - :type fget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]] + :type fget: Callable[[_OwnerT], _ReturnT] | None :param fset: normal setter. - :type fset: typing.Optional[typing.Callable[[typing.Any, typing.Any], None]] + :type fset: Callable[[_OwnerT, _ReturnT], None] | None :param fdel: normal deleter. - :type fdel: typing.Optional[typing.Callable[[typing.Any, ], None]] + :type fdel: Callable[[_OwnerT], None] | None :param doc: docstring override - :type doc: typing.Optional[str] + :type doc: str | None :param logger: logger instance or name to use as override - :type logger: typing.Optional[typing.Union[logging.Logger, str]] + :type logger: logging.Logger | str | None :param log_object_repr: use `repr` over object to describe owner if True else owner class name and id :type log_object_repr: bool :param log_level: log level for successful operations @@ -83,7 +89,7 @@ def __init__( :param log_traceback: Log traceback on exceptions :type log_traceback: bool :param override_name: override property name if not None else use getter/setter/deleter name - :type override_name: typing.Optional[str] + :type override_name: str | None Usage examples: @@ -171,11 +177,15 @@ def __init__( >>> logs[27] == 'Traceback (most recent call last):' True """ - warnings.warn("LogOnAccess has been ported to logwrap with extended repr logic.", DeprecationWarning) + warnings.warn( + "LogOnAccess has been ported to logwrap with extended repr logic.", + DeprecationWarning, + stacklevel=2, + ) super().__init__(fget=fget, fset=fset, fdel=fdel, doc=doc) if logger is None or isinstance(logger, logging.Logger): - self.__logger: typing.Optional[logging.Logger] = logger + self.__logger: logging.Logger | None = logger else: self.__logger = logging.getLogger(logger) @@ -185,15 +195,15 @@ def __init__( self.__log_success: bool = log_success self.__log_failure: bool = log_failure self.__log_traceback: bool = log_traceback - self.__override_name: typing.Optional[str] = override_name + self.__override_name: str | None = override_name self.__name: str = "" - self.__owner: typing.Optional[typing.Type[_OwnerT]] = None + self.__owner: type[_OwnerT] | None = None - def __set_name__(self, owner: typing.Optional[typing.Type[_OwnerT]], name: str) -> None: + def __set_name__(self, owner: type[_OwnerT] | None, name: str) -> None: """Set __name__ and __objclass__ property. :param owner: owner class, where descriptor applied - :type owner: typing.Optional[type] + :type owner: type[_OwnerT] | None :param name: descriptor name :type name: str """ @@ -201,11 +211,11 @@ def __set_name__(self, owner: typing.Optional[typing.Type[_OwnerT]], name: str) self.__name = name @property - def __objclass__(self) -> typing.Optional[typing.Type[_OwnerT]]: # pragma: no cover + def __objclass__(self) -> type[_OwnerT] | None: # pragma: no cover """Read-only owner. :return: property owner class - :rtype: typing.Optional[type] + :rtype: type[_OwnerT] | None """ return self.__owner @@ -220,19 +230,19 @@ def __traceback(self) -> str: return "" exc_info = sys.exc_info() stack: traceback.StackSummary = traceback.extract_stack() - full_tb: typing.List[traceback.FrameSummary] = [elem for elem in stack if elem.filename != _CURRENT_FILE] - exc_line: typing.List[str] = traceback.format_exception_only(*exc_info[:2]) + full_tb: list[traceback.FrameSummary] = [elem for elem in stack if elem.filename != _CURRENT_FILE] + exc_line: list[str] = traceback.format_exception_only(*exc_info[:2]) # Make standard traceback string tb_text = "\nTraceback (most recent call last):\n" + "".join(traceback.format_list(full_tb)) + "".join(exc_line) return tb_text - def __get_obj_source(self, instance: _OwnerT, owner: typing.Optional[typing.Type[_OwnerT]] = None) -> str: + def __get_obj_source(self, instance: _OwnerT, owner: type[_OwnerT] | None = None) -> str: """Get object repr block. :param instance: object instance - :type instance: typing.Any + :type instance: _OwnerT :param owner: object class (available for getter usage only) - :type owner: typing.Optional[type] + :type owner: type[_OwnerT] | None :return: repr of object if it not disabled else repr placeholder :rtype: str """ @@ -248,7 +258,7 @@ def _get_logger_for_instance(self, instance: _OwnerT) -> logging.Logger: """Get logger for log calls. :param instance: Owner class instance. Filled only if instance created, else None. - :type instance: typing.Optional[owner] + :type instance: _OwnerT :return: logger instance :rtype: logging.Logger """ @@ -269,13 +279,14 @@ def _get_logger_for_instance(self, instance: _OwnerT) -> logging.Logger: def __get__( self, instance: None, - owner: typing.Optional[typing.Type[_OwnerT]] = None, + owner: type[_OwnerT] | None = None, ) -> typing.NoReturn: """Get descriptor. :param instance: Owner class instance. Filled only if instance created, else None. - :type instance: typing.Optional[owner] + :type instance: None :param owner: Owner class for property. + :type owner: type[_OwnerT] | None :return: getter call result if getter presents :rtype: typing.Any :raises AttributeError: Getter is not available @@ -286,13 +297,14 @@ def __get__( def __get__( self, instance: _OwnerT, - owner: typing.Optional[typing.Type[_OwnerT]] = None, + owner: type[_OwnerT] | None = None, ) -> _ReturnT: # noqa: F811 """Get descriptor. :param instance: Owner class instance. Filled only if instance created, else None. - :type instance: typing.Optional[owner] + :type instance: _OwnerT :param owner: Owner class for property. + :type owner: type[_OwnerT] | None :return: getter call result if getter presents :rtype: typing.Any :raises AttributeError: Getter is not available @@ -301,14 +313,15 @@ def __get__( def __get__( self, - instance: typing.Optional[_OwnerT], - owner: typing.Optional[typing.Type[_OwnerT]] = None, + instance: _OwnerT | None, + owner: type[_OwnerT] | None = None, ) -> _ReturnT: # noqa: F811 """Get descriptor. :param instance: Owner class instance. Filled only if instance created, else None. - :type instance: typing.Optional[owner] + :type instance: _OwnerT | None :param owner: Owner class for property. + :type owner: type[_OwnerT] | None :return: getter call result if getter presents :rtype: typing.Any :raises AttributeError: Getter is not available @@ -324,7 +337,7 @@ def __get__( result = super().__get__(instance, owner) if self.log_success: logger.log(self.log_level, f"{source}.{self.__name__} -> {result!r}") - return result # type: ignore + return result # type: ignore[no-any-return] except Exception: if self.log_failure: logger.log(self.exc_level, f"Failed: {source}.{self.__name__}{self.__traceback}", exc_info=False) @@ -334,7 +347,7 @@ def __set__(self, instance: _OwnerT, value: _ReturnT) -> None: """Set descriptor. :param instance: Owner class instance. Filled only if instance created, else None. - :type instance: typing.Optional + :type instance: _OwnerT :param value: Value for setter :raises AttributeError: Setter is not available :raises Exception: Something goes wrong @@ -360,7 +373,7 @@ def __delete__(self, instance: _OwnerT) -> None: """Delete descriptor. :param instance: Owner class instance. Filled only if instance created, else None. - :type instance: typing.Optional + :type instance: _OwnerT :raises AttributeError: Deleter is not available :raises Exception: Something goes wrong """ @@ -380,20 +393,20 @@ def __delete__(self, instance: _OwnerT) -> None: raise @property - def logger(self) -> typing.Optional[logging.Logger]: + def logger(self) -> logging.Logger | None: """Logger instance to use as override. :return: logger instance if set - :rtype: typing.Optional[logging.Logger] + :rtype: logging.Logger | None """ return self.__logger @logger.setter - def logger(self, logger: typing.Union[logging.Logger, str, None]) -> None: + def logger(self, logger: logging.Logger | str | None) -> None: """Logger instance to use as override. :param logger: logger instance, logger name or None if override disable required - :type logger: typing.Union[logging.Logger, str, None] + :type logger: logging.Logger | str | None """ if logger is None or isinstance(logger, logging.Logger): self.__logger = logger @@ -509,20 +522,20 @@ def log_traceback(self, value: bool) -> None: self.__log_traceback = value @property - def override_name(self) -> typing.Optional[str]: + def override_name(self) -> str | None: """Override property name if not None else use getter/setter/deleter name. :return: property name override - :rtype: typing.Optional[str] + :rtype: str | None """ return self.__override_name @override_name.setter - def override_name(self, name: typing.Optional[str]) -> None: + def override_name(self, name: str | None) -> None: """Override property name if not None else use getter/setter/deleter name. :param name: property name override - :type name: typing.Optional[str] + :type name: str | None """ self.__override_name = name diff --git a/advanced_descriptors/separate_class_method.py b/advanced_descriptors/separate_class_method.py index 72595f0..4a03e00 100755 --- a/advanced_descriptors/separate_class_method.py +++ b/advanced_descriptors/separate_class_method.py @@ -1,6 +1,6 @@ #!/usr/bin/env python -# Copyright 2017 - 2021 Alexey Stepanov aka penguinolog +# Copyright 2017 - 2022 Alexey Stepanov aka penguinolog # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at @@ -21,6 +21,10 @@ import functools import typing +if typing.TYPE_CHECKING: + # Standard Library + from collections.abc import Callable + __all__ = ("SeparateClassMethod",) _MethodReturnT = typing.TypeVar("_MethodReturnT") @@ -99,43 +103,43 @@ class SeparateClassMethod(typing.Generic[_MethodReturnT, _ClassMethodReturnT]): def __init__( self, - imeth: typing.Optional[typing.Callable[..., _MethodReturnT]] = None, - cmeth: typing.Optional[typing.Callable[..., _ClassMethodReturnT]] = None, + imeth: Callable[..., _MethodReturnT] | None = None, + cmeth: Callable[..., _ClassMethodReturnT] | None = None, ) -> None: """Separate class method and instance methods. :param imeth: Instance method - :type imeth: typing.Optional[typing.Callable] + :type imeth: Callable[..., _MethodReturnT] | None :param cmeth: Class method - :type cmeth: typing.Optional[typing.Callable] + :type cmeth: Callable[..., _ClassMethodReturnT] | None """ - self.__instance_method: typing.Optional[typing.Callable[..., _MethodReturnT]] = imeth - self.__class_method: typing.Optional[typing.Callable[..., _ClassMethodReturnT]] = cmeth - self.__owner: typing.Optional[type] = None + self.__instance_method: Callable[..., _MethodReturnT] | None = imeth + self.__class_method: Callable[..., _ClassMethodReturnT] | None = cmeth + self.__owner: type | None = None self.__name: str = "" - def __set_name__(self, owner: typing.Optional[type], name: str) -> None: + def __set_name__(self, owner: type | None, name: str) -> None: """Set __name__ and __objclass__ property.""" self.__owner = owner self.__name = name @typing.overload - def __get__(self, instance: None, owner: typing.Any) -> typing.Callable[..., _ClassMethodReturnT]: + def __get__(self, instance: None, owner: typing.Any) -> Callable[..., _ClassMethodReturnT]: """Class method.""" @typing.overload - def __get__(self, instance: typing.Any, owner: typing.Any) -> typing.Callable[..., _MethodReturnT]: + def __get__(self, instance: typing.Any, owner: typing.Any) -> Callable[..., _MethodReturnT]: """Normal method.""" def __get__( self, - instance: typing.Optional[typing.Any], + instance: typing.Any | None, owner: typing.Any, - ) -> typing.Callable[..., typing.Union[_MethodReturnT, _ClassMethodReturnT]]: + ) -> Callable[..., _MethodReturnT | _ClassMethodReturnT]: """Get descriptor. :return: class method or instance method depends on call behavior - :rtype: typing.Callable + :rtype: Callable :raises AttributeError: Not implemented getter for class method and called class context. """ if instance is None or self.__instance_method is None: @@ -149,7 +153,7 @@ def class_method(*args: typing.Any, **kwargs: typing.Any) -> _ClassMethodReturnT :return: bound class method result :rtype: typing.Any """ - return self.__class_method(owner, *args, **kwargs) # type: ignore + return self.__class_method(owner, *args, **kwargs) # type: ignore[misc] return class_method @@ -160,16 +164,16 @@ def instance_method(*args: typing.Any, **kwargs: typing.Any) -> _MethodReturnT: :return: bound instance method result :rtype: typing.Any """ - return self.__instance_method(instance, *args, **kwargs) # type: ignore + return self.__instance_method(instance, *args, **kwargs) # type: ignore[misc] return instance_method @property - def __objclass__(self) -> typing.Optional[type]: # pragma: no cover + def __objclass__(self) -> type | None: # pragma: no cover """Read-only owner. :return: property owner class - :rtype: typing.Optional[type] + :rtype: type | None """ return self.__owner @@ -184,45 +188,45 @@ def __name__(self) -> str: # pragma: no cover def instance_method( self, - imeth: typing.Optional[typing.Callable[..., _MethodReturnT]], + imeth: Callable[..., _MethodReturnT] | None, ) -> SeparateClassMethod[_MethodReturnT, _ClassMethodReturnT]: """Descriptor to change instance method. :param imeth: New instance method. - :type imeth: typing.Optional[typing.Callable] + :type imeth: Callable[..., _MethodReturnT] | None :return: SeparateClassMethod - :rtype: SeparateClassMethod + :rtype: SeparateClassMethod[_MethodReturnT, _ClassMethodReturnT] """ self.__instance_method = imeth return self def class_method( self, - cmeth: typing.Optional[typing.Callable[..., _ClassMethodReturnT]], + cmeth: Callable[..., _ClassMethodReturnT] | None, ) -> SeparateClassMethod[_MethodReturnT, _ClassMethodReturnT]: """Descriptor to change class method. :param cmeth: New class method. - :type cmeth: typing.Optional[typing.Callable] + :type cmeth: Callable[..., _ClassMethodReturnT] | None :return: SeparateClassMethod - :rtype: SeparateClassMethod + :rtype: SeparateClassMethod[_MethodReturnT, _ClassMethodReturnT] """ self.__class_method = cmeth return self @property - def imeth(self) -> typing.Optional[typing.Callable[..., _MethodReturnT]]: + def imeth(self) -> Callable[..., _MethodReturnT] | None: """Instance method instance. - :rtype: typing.Optional[typing.Callable] + :rtype: Callable[..., _MethodReturnT] | None """ return self.__instance_method @property - def cmeth(self) -> typing.Optional[typing.Callable[..., _ClassMethodReturnT]]: + def cmeth(self) -> Callable[..., _ClassMethodReturnT] | None: """Class method instance. - :rtype: typing.Optional[typing.Callable] + :rtype: Callable[..., _ClassMethodReturnT] | None """ return self.__class_method diff --git a/build_requirements.txt b/build_requirements.txt deleted file mode 100644 index 0d9ac7a..0000000 --- a/build_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -Cython; platform_python_implementation == "CPython" --r CI_REQUIREMENTS.txt diff --git a/classifiers.txt b/classifiers.txt new file mode 100644 index 0000000..43a8215 --- /dev/null +++ b/classifiers.txt @@ -0,0 +1,13 @@ +Development Status :: 5 - Production/Stable +Intended Audience :: Developers +Topic :: Software Development :: Libraries :: Python Modules +License :: OSI Approved :: Apache Software License +Programming Language :: Python :: 3 +Programming Language :: Python :: 3 :: Only +Programming Language :: Python :: 3.7 +Programming Language :: Python :: 3.8 +Programming Language :: Python :: 3.9 +Programming Language :: Python :: 3.10 +Programming Language :: Python :: 3.11 +Programming Language :: Python :: Implementation :: CPython +Programming Language :: Python :: Implementation :: PyPy diff --git a/doc/source/advanced_property.rst b/doc/source/advanced_property.rst index 729a1ff..4c10c25 100644 --- a/doc/source/advanced_property.rst +++ b/doc/source/advanced_property.rst @@ -16,13 +16,13 @@ API: AdvancedProperty .. py:method:: __init__(fget=None, fset=None, fdel=None, fcget=None, ) :param fget: normal getter. - :type fget: ``typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]`` + :type fget: ``Callable[[_OwnerClassT], _ReturnT] | None`` :param fset: normal setter. - :type fset: ``typing.Optional[typing.Callable[[typing.Any, typing.Any], None]]`` + :type fset: ``Callable[[_OwnerClassT, _ReturnT], None] | None`` :param fdel: normal deleter. - :type fdel: ``typing.Optional[typing.Callable[[typing.Any, ], None]]`` + :type fdel: ``Callable[[_OwnerClassT], None] | None`` :param fcget: class getter. Used as normal, if normal is None. - :type fcget: ``typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]`` + :type fcget: ``Callable[[type[_OwnerClassT]], _ClassReturnT] | None`` .. note:: doc argument is not supported due to class wide getter usage. @@ -31,7 +31,7 @@ API: AdvancedProperty Descriptor to change the getter on a property. :param fget: new normal getter. - :type fget: ``typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]`` + :type fget: ``Callable[[_OwnerClassT], _ReturnT] | None`` :rtype: ``AdvancedProperty`` .. py:method:: setter(fset) @@ -39,7 +39,7 @@ API: AdvancedProperty Descriptor to change the setter on a property. :param fset: new setter. - :type fset: ``typing.Optional[typing.Callable[[typing.Any, typing.Any], None]]`` + :type fset: ``Callable[[_OwnerClassT, _ReturnT], None] | None`` :rtype: ``AdvancedProperty`` .. py:method:: deleter(fdel) @@ -47,7 +47,7 @@ API: AdvancedProperty Descriptor to change the deleter on a property. :param fdel: New deleter. - :type fdel: ``typing.Optional[typing.Callable[[typing.Any, ], None]]`` + :type fdel: ``Callable[[_OwnerClassT], None] | None`` :rtype: ``AdvancedProperty`` .. py:method:: cgetter(fcget) @@ -55,25 +55,25 @@ API: AdvancedProperty Descriptor to change the class wide getter on a property. :param fcget: new class-wide getter. - :type fcget: ``typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]`` + :type fcget: ``Callable[[type[_OwnerClassT]], _ClassReturnT] | None`` :rtype: ``AdvancedProperty`` .. py:attribute:: fget - ``typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]`` + ``Callable[[_OwnerClassT], _ReturnT] | None`` Getter instance. .. py:attribute:: fset - ``typing.Optional[typing.Callable[[typing.Any, typing.Any], None]]`` + ``Callable[[_OwnerClassT, _ReturnT], None] | None`` Setter instance. .. py:attribute:: fdel - ``typing.Optional[typing.Callable[[typing.Any, ], None]]`` + ``Callable[[_OwnerClassT], None] | None`` Deleter instance. .. py:attribute:: fcget - ``typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]`` + ``Callable[[type[_OwnerClassT]], _ClassReturnT] | None`` Class wide getter instance. diff --git a/doc/source/log_on_access.rst b/doc/source/log_on_access.rst index f796b39..e2d35db 100644 --- a/doc/source/log_on_access.rst +++ b/doc/source/log_on_access.rst @@ -17,36 +17,36 @@ API: LogOnAccess .. py:method:: __init__(fget=None, fset=None, fdel=None, doc=None, *, logger=None, log_object_repr=True, log_level=logging.DEBUG, exc_level=logging.DEBUG, log_success=True, log_failure=True, log_traceback=True, override_name=None) :param fget: normal getter. - :type fget: typing.Optional[typing.Callable[[typing.Any, ], typing.Any]] + :type fget: ``Callable[[_OwnerT], _ReturnT] | None`` :param fset: normal setter. - :type fset: typing.Optional[typing.Callable[[typing.Any, typing.Any], None]] + :type fset: ``Callable[[_OwnerT, _ReturnT], None] | None`` :param fdel: normal deleter. - :type fdel: typing.Optional[typing.Callable[[typing.Any, ], None]] + :type fdel: ``Callable[[_OwnerT], None] | None`` :param doc: docstring override - :type doc: typing.Optional[str] + :type doc: ``str | None`` :param logger: logger instance or name to use as override - :type logger: typing.Optional[typing.Union[logging.Logger, str]] + :type logger: ``logging.Logger | str | None`` :param log_object_repr: use `repr` over object to describe owner if True else owner class name and id - :type log_object_repr: bool + :type log_object_repr: ``bool`` :param log_level: log level for successful operations - :type log_level: int + :type log_level: ``int`` :param exc_level: log level for exceptions - :type exc_level: int + :type exc_level: ``int`` :param log_success: log successful operations - :type log_success: bool + :type log_success: ``bool`` :param log_failure: log exceptions - :type log_failure: bool + :type log_failure: ``bool`` :param log_traceback: Log traceback on exceptions - :type log_traceback: bool + :type log_traceback: ``bool`` :param override_name: override property name if not None else use getter/setter/deleter name - :type override_name: typing.Optional[str] + :type override_name: ``str | None`` .. py:method:: getter(fget) Descriptor to change the getter on a property. :param fget: new normal getter. - :type fget: ``typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]`` + :type fget: ``Callable[[_OwnerT], _ReturnT] | None`` :rtype: ``AdvancedProperty`` .. py:method:: setter(fset) @@ -54,7 +54,7 @@ API: LogOnAccess Descriptor to change the setter on a property. :param fset: new setter. - :type fset: ``typing.Optional[typing.Callable[[typing.Any, typing.Any], None]]`` + :type fset: ``Callable[[_OwnerT, _ReturnT], None] | None`` :rtype: ``AdvancedProperty`` .. py:method:: deleter(fdel) @@ -62,27 +62,27 @@ API: LogOnAccess Descriptor to change the deleter on a property. :param fdel: New deleter. - :type fdel: ``typing.Optional[typing.Callable[[typing.Any, ], None]]`` + :type fdel: ``Callable[[_OwnerT], None] | None`` :rtype: ``AdvancedProperty`` .. py:attribute:: fget - ``typing.Optional[typing.Callable[[typing.Any, ], typing.Any]]`` + ``Callable[[_OwnerT], _ReturnT] | None`` Getter instance. .. py:attribute:: fset - ``typing.Optional[typing.Callable[[typing.Any, typing.Any], None]]`` + ``Callable[[_OwnerT, _ReturnT], None] | None`` Setter instance. .. py:attribute:: fdel - ``typing.Optional[typing.Callable[[typing.Any, ], None]]`` + ``Callable[[_OwnerT], None] | None`` Deleter instance. .. py:attribute:: logger - ``typing.Optional[logging.Logger]`` + ``logging.Logger | None`` Logger instance to use as override. .. py:attribute:: log_object_repr @@ -117,5 +117,5 @@ API: LogOnAccess .. py:attribute:: override_name - ``typing.Optional[str]`` + ``str | None`` Override property name if not None else use getter/setter/deleter name. diff --git a/doc/source/separate_classmethod.rst b/doc/source/separate_classmethod.rst index 4078ee2..383c089 100644 --- a/doc/source/separate_classmethod.rst +++ b/doc/source/separate_classmethod.rst @@ -14,16 +14,16 @@ API: SeparateClassMethod .. py:method:: __init__(imeth=None, cmeth=None, ) :param imeth: Instance method - :type imeth: ``typing.Optional[typing.Callable]`` + :type imeth: ``Callable[..., _MethodReturnT] | None`` :param cmeth: Class method - :type cmeth: ``typing.Optional[typing.Callable]`` + :type cmeth: ``Callable[..., _ClassMethodReturnT] | None`` .. py:method:: instance_method(imeth) Descriptor to change instance method. :param imeth: New instance method. - :type imeth: ``typing.Optional[typing.Callable]`` + :type imeth: ``Callable[..., _MethodReturnT] | None`` :rtype: ``SeparateClassMethod`` .. py:method:: class_method(cmeth) @@ -31,15 +31,15 @@ API: SeparateClassMethod Descriptor to change class method. :type cmeth: New class method. - :type cmeth: ``typing.Optional[typing.Callable]`` + :type cmeth: ``Callable[..., _ClassMethodReturnT] | None`` :rtype: ``SeparateClassMethod`` .. py:attribute:: imeth - ``typing.Optional[typing.Callable]`` + ``Callable[..., _MethodReturnT] | None`` Instance method instance. .. py:attribute:: cmeth - ``typing.Optional[typing.Callable]`` + ``Callable[..., _ClassMethodReturnT] | None`` Class method instance. diff --git a/pyproject.toml b/pyproject.toml index e7831af..6c9e5c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,16 +2,54 @@ # Minimum requirements for the build system to execute. # PEP 508 specifications for PEP 518. requires = [ - "setuptools >= 21.0.0,!=24.0.0,!=34.0.0,!=34.0.1,!=34.0.2,!=34.0.3,!=34.1.0,!=34.1.1,!=34.2.0,!=34.3.0,!=34.3.1,!=34.3.2,!=36.2.0", # PSF/ZPL - "wheel", - "setuptools_scm[toml]>=3.4", + "setuptools>=45", # PSF/ZPL + "setuptools_scm[toml]>=6.2", ] build-backend="setuptools.build_meta" +[project] +name = "advanced-descriptors" +description = "Advanced descriptors for special cases." +readme = "README.rst" +urls={"Bug Tracker" = "https://github.com/python-useful-helpers/advanced-descriptors/issues", "Documentation" = "https://advanced-descriptors.readthedocs.io/" } +requires-python = ">=3.7.0" +keywords = ["descriptor", "property", "classmethod", "development"] +license = {text = "Apache License, Version 2.0"} +authors=[{name="Alexey Stepanov", email="penguinolog@gmail.com"}] +maintainers=[ + {name="Alexey Stepanov", email="penguinolog@gmail.com"}, + {name="Antonio Esposito", email="esposito.cloud@gmail.com"}, + {name="Dennis Dmitriev", email="dis-xcom@gmail.com"} +] +dynamic = ["version", "classifiers", "dependencies"] + +[tool.setuptools.package-data] +advanced_descriptors=[ + "py.typed", + "*.pyi", + "*/*.pyi" +] + +[tool.setuptools.packages.find] +exclude = [ + "doc", + "test", + ".*" +] +include = ["advanced_descriptors"] +namespaces = false + +[tool.setuptools.dynamic] +dependencies = {file = ["requirements.txt"]} +classifiers = {file = ["classifiers.txt"]} + +[tool.distutils.bdist_wheel] +universal = 0 + [tool.black] line-length = 120 safe = true -target-version = ["py36", "py37", "py38"] +target-version = ["py37", "py38"] [tool.isort] line_length = 120 @@ -23,6 +61,38 @@ import_heading_thirdparty = "External Dependencies" import_heading_firstparty = "Package Implementation" import_heading_localfolder = "Local Implementation" +[tool.doc8] +max-line-length = 150 + +[tool.pydocstyle] +ignore = [ + "D401", + "D202", + "D203", + "D213" +] +# First line should be in imperative mood; try rephrasing +# No blank lines allowed after function docstring +# 1 blank line required before class docstring +# Multi-line docstring summary should start at the second line +match = "(?!_version|test_)*.py" + +[tool.mypy] +strict = true +warn_unused_configs = true +warn_redundant_casts = true +show_error_context = true +show_column_numbers = true +show_error_codes = true +pretty = true + +[tool.pytest.ini_options] +minversion = "6.0" +addopts = "-vvv -s -p no:django -p no:ipdb" +testpaths = ["test"] +mock_use_standalone_module = false +junit_family = "xunit2" + [tool.coverage.run] omit = ["test/*"] branch = true @@ -65,3 +135,4 @@ exclude_lines = [ pretty_print = true [tool.setuptools_scm] +write_to = "advanced_descriptors/_version.py" diff --git a/pytest.ini b/pytest.ini deleted file mode 100644 index b44d5a0..0000000 --- a/pytest.ini +++ /dev/null @@ -1,5 +0,0 @@ -[pytest] -addopts = -vvv -s -p no:django -p no:ipdb -testpaths = test -mock_use_standalone_module = false -junit_family = xunit2 diff --git a/pytest_requirements.txt b/pytest_requirements.txt index 2504209..685006f 100644 --- a/pytest_requirements.txt +++ b/pytest_requirements.txt @@ -1,4 +1,4 @@ -pytest +pytest >= 6.0 pytest-cov pytest-mock # pytest-sugar diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 8b91738..0000000 --- a/setup.cfg +++ /dev/null @@ -1,68 +0,0 @@ -[metadata] -project_urls = - Bug Tracker = https://github.com/python-useful-helpers/advanced-descriptors/issues - Documentation = https://advanced-descriptors.readthedocs.io/ - -long_description = file: README.rst - -[options] -zip_safe = True -packages = find: - -[bdist_wheel] -# This flag says that the code is written to work on both Python 2 and Python -# 3. If at all possible, it is good practice to do this. If you cannot, you -# will need to generate wheels for each Python version that you support. -universal = 0 - -[easy_install] -zip_ok = True - -[build_sphinx] -all_files = 1 -build-dir = doc/build -source-dir = doc/source - -[flake8] -exclude = - .venv, - .git, - .tox, - dist, - doc, - *lib/python*, - *egg, - build, - __init__.py, - _version.py, - docs -ignore = - E203, -# whitespace before ':' - W503, -# line break before binary operator - D401, -# First line should be in imperative mood; try rephrasing - D202, -# No blank lines allowed after function docstring - D203, -# 1 blank line required before class docstring - D213 -# Multi-line docstring summary should start at the second line -show-pep8 = True -show-source = True -count = True -max-line-length = 120 - -[doc8] -max-line-length = 150 - -[aliases] -test = pytest - -[mypy] -warn_unused_configs = True -warn_redundant_casts = True -show_error_context = True -show_column_numbers = True -pretty = True diff --git a/setup.py b/setup.py index d802931..1fcdd27 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,10 @@ -# Copyright 2016 - 2021 Alexey Stepanov aka penguinolog +# Copyright 2016 - 2022 Alexey Stepanov aka penguinolog # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at - +# # http://www.apache.org/licenses/LICENSE-2.0 - +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the @@ -15,153 +15,7 @@ from __future__ import annotations -# Standard Library -import ast -import os.path -import sys - # External Dependencies import setuptools -import typing - - -try: - # noinspection PyPackageRequirements - from Cython.Build import cythonize -except ImportError: - cythonize = None - -PACKAGE_NAME = "advanced_descriptors" - -with open(os.path.join(os.path.dirname(__file__), PACKAGE_NAME, "__init__.py")) as f: - SOURCE = f.read() - -with open("requirements.txt") as f: - REQUIRED = f.read().splitlines() - -with open("README.rst") as f: - LONG_DESCRIPTION = f.read() - - -# noinspection PyUnresolvedReferences -def get_simple_vars_from_src( - src: str, -) -> typing.Dict[str, typing.Union[str, bytes, int, float, complex, list, set, dict, tuple, None, bool, Ellipsis]]: - """Get simple (string/number/boolean and None) assigned values from source. - - :param src: Source code - :type src: str - :return: OrderedDict with keys, values = variable names, values - :rtype: typing.Dict[ - str, - typing.Union[ - str, bytes, - int, float, complex, - list, set, dict, tuple, - None, bool, Ellipsis - ] - ] - - Limitations: Only defined from scratch variables. - Not supported by design: - * Imports - * Executable code, including string formatting and comprehensions. - - Examples: - >>> string_sample = "a = '1'" - >>> get_simple_vars_from_src(string_sample) - {'a': '1'} - - >>> int_sample = "b = 1" - >>> get_simple_vars_from_src(int_sample) - {'b': 1} - - >>> list_sample = "c = [u'1', b'1', 1, 1.0, 1j, None]" - >>> result = get_simple_vars_from_src(list_sample) - >>> result == {'c': [u'1', b'1', 1, 1.0, 1j, None]} - True - - >>> iterable_sample = "d = ([1], {1: 1}, {1})" - >>> get_simple_vars_from_src(iterable_sample) - {'d': ([1], {1: 1}, {1})} - - >>> multiple_assign = "e = f = g = 1" - >>> get_simple_vars_from_src(multiple_assign) - {'e': 1, 'f': 1, 'g': 1} - """ - if sys.version_info[:2] < (3, 8): - ast_data = (ast.Str, ast.Num, ast.List, ast.Set, ast.Dict, ast.Tuple, ast.Bytes, ast.NameConstant, ast.Ellipsis) - else: - ast_data = (ast.Constant, ast.List, ast.Set, ast.Dict, ast.Tuple) - - tree = ast.parse(src) - - result = {} - - for node in ast.iter_child_nodes(tree): - # We parse assigns only - if not isinstance(node, (ast.Assign, ast.AnnAssign)) or not isinstance(node.value, ast_data): - continue - try: - value = ast.literal_eval(node.value) - except ValueError: - continue - if isinstance(node, ast.Assign): - for tgt in node.targets: - if isinstance(tgt, ast.Name) and isinstance(tgt.ctx, ast.Store): - result[tgt.id] = value - else: - result[node.target.id] = value - return result - - -VARIABLES = get_simple_vars_from_src(SOURCE) - -CLASSIFIERS = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "Topic :: Software Development :: Libraries :: Python Modules", - "License :: OSI Approved :: Apache Software License", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: Implementation :: CPython", - "Programming Language :: Python :: Implementation :: PyPy", -] - -KEYWORDS = ["descriptor", "property", "classmethod", "development"] -setuptools.setup( - name="advanced-descriptors", - author=VARIABLES["__author__"], - author_email=VARIABLES["__author_email__"], - maintainer=", ".join( - "{name} <{email}>".format(name=name, email=email) for name, email in VARIABLES["__maintainers__"].items() - ), - url=VARIABLES["__url__"], - license=VARIABLES["__license__"], - description=VARIABLES["__description__"], - long_description=LONG_DESCRIPTION, - classifiers=CLASSIFIERS, - keywords=KEYWORDS, - python_requires=">=3.7.0", - # While setuptools cannot deal with pre-installed incompatible versions, - # setting a lower bound is not harmful - it makes error messages cleaner. DO - # NOT set an upper bound on setuptools, as that will lead to uninstallable - # situations as progressive releases of projects are done. - # Blacklist setuptools 34.0.0-34.3.2 due to https://github.com/pypa/setuptools/issues/951 - # Blacklist setuptools 36.2.0 due to https://github.com/pypa/setuptools/issues/1086 - setup_requires=[ - "setuptools >= 21.0.0,!=24.0.0," - "!=34.0.0,!=34.0.1,!=34.0.2,!=34.0.3,!=34.1.0,!=34.1.1,!=34.2.0,!=34.3.0,!=34.3.1,!=34.3.2," - "!=36.2.0", - "wheel", - "setuptools_scm[toml]>=3.4", - ], - use_scm_version={"write_to": f"{PACKAGE_NAME}/_version.py"}, - install_requires=REQUIRED, - package_data={PACKAGE_NAME: ["py.typed"]}, -) +setuptools.setup() diff --git a/test/test_advanced_property.py b/test/test_advanced_property.py index 07184ab..ffc2188 100644 --- a/test/test_advanced_property.py +++ b/test/test_advanced_property.py @@ -2,8 +2,10 @@ """Tests for AdvancedProperty.""" +# Standard Library import unittest +# Package Implementation import advanced_descriptors diff --git a/test/test_log_on_access.py b/test/test_log_on_access.py index 717c1cb..88768b3 100644 --- a/test/test_log_on_access.py +++ b/test/test_log_on_access.py @@ -2,10 +2,12 @@ """Tests for LogOnAccess descriptor.""" +# Standard Library import io import logging import unittest +# Package Implementation import advanced_descriptors @@ -71,7 +73,7 @@ def __init__(self, val="ok"): self.val = val def __repr__(self): - return "{cls}(val={self.val})".format(cls=self.__class__.__name__, self=self) + return f"{self.__class__.__name__}(val={self.val})" @advanced_descriptors.LogOnAccess def ok(self): @@ -97,7 +99,7 @@ def __init__(self, val="ok"): self.val = val def __repr__(self): - return "{cls}(val={self.val})".format(cls=self.__class__.__name__, self=self) + return f"{self.__class__.__name__}(val={self.val})" @advanced_descriptors.LogOnAccess def ok(self): @@ -168,7 +170,7 @@ def __init__(self, val="ok"): self.val = val def __repr__(self): - return "{cls}(val={self.val})".format(cls=self.__class__.__name__, self=self) + return f"{self.__class__.__name__}(val={self.val})" @advanced_descriptors.LogOnAccess def ok(self): @@ -198,7 +200,7 @@ def __init__(self, val="ok"): self.val = val def __repr__(self): - return "{cls}(val={self.val})".format(cls=self.__class__.__name__, self=self) + return f"{self.__class__.__name__}(val={self.val})" @advanced_descriptors.LogOnAccess def ok(self): @@ -291,7 +293,7 @@ def __init__(self, val="ok"): self.logger = logging.getLogger(self.__class__.__name__) def __repr__(self): - return "{cls}(val={self.val})".format(cls=self.__class__.__name__, self=self) + return f"{self.__class__.__name__}(val={self.val})" @advanced_descriptors.LogOnAccess def ok(self): diff --git a/test/test_separate_class_method.py b/test/test_separate_class_method.py index 3b5df4b..3c31691 100644 --- a/test/test_separate_class_method.py +++ b/test/test_separate_class_method.py @@ -2,8 +2,10 @@ """Tests for SeparateClassMethod.""" +# Standard Library import unittest +# Package Implementation import advanced_descriptors @@ -13,7 +15,7 @@ class TestSeparateClassMethod(unittest.TestCase): def test_01_instance_method(self): """Test instance method support with decorating in-place.""" - class Target(object): + class Target: def __init__(tself): tself.value = 42 @@ -29,7 +31,7 @@ def getval(tself): def test_02_alt_instance_method(self): """Test instance method support with late bind.""" - class Target(object): + class Target: def __init__(tself): tself.value = 42 @@ -47,7 +49,7 @@ def getval(tself): def test_03_class_method(self): """Test class method support.""" - class Target(object): + class Target: getcls = advanced_descriptors.SeparateClassMethod() @getcls.class_method @@ -60,7 +62,7 @@ def getcls(cls): def test_04_both(self): """Test coexistency of class method and instance method.""" - class Target(object): + class Target: value = 1 def __init__(tself): @@ -87,7 +89,7 @@ def imeth(instance): def cmeth(owner): return owner.value - class Target(object): + class Target: value = 1 def __init__(tself): diff --git a/tox.ini b/tox.ini index a3135a8..26bcb81 100644 --- a/tox.ini +++ b/tox.ini @@ -5,14 +5,20 @@ [tox] minversion = 3.15 -envlist = black, pep8, pylint, mypy, bandit, pep257, py3{7,8,9,10}, docs +envlist = black, pep8, pylint, mypy, bandit, pep257, py3{7,8,9,10,11}, readme, doc8, docs skipsdist = True skip_missing_interpreters = True [testenv] recreate = True usedevelop = False -passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY +passenv = + http_proxy + HTTP_PROXY + https_proxy + HTTPS_PROXY + no_proxy + NO_PROXY setev = PYTHONDONTWRITEBYTECODE=1 deps = sphinx @@ -22,7 +28,7 @@ deps = commands = pip freeze - python setup.py develop -v + pip install -e . py.test --cov-report html --self-contained-html --html=report.html --cov=advanced_descriptors coverage report --fail-under 89 @@ -93,10 +99,41 @@ deps = -r{toxinidir}/CI_REQUIREMENTS.txt commands = python setup.py --version clean - mypy --strict --install-types --non-interactive --xslt-html-report mypy_report -p advanced_descriptors + mypy --install-types --non-interactive --xslt-html-report mypy_report -p advanced_descriptors [testenv:isort] deps = isort commands = isort advanced_descriptors + +[flake8] +exclude = + .venv, + .git, + .tox, + dist, + doc, + *lib/python*, + *egg, + build, + __init__.py, + _version.py, + docs +ignore = + E203, +# whitespace before ':' + W503, +# line break before binary operator + D401, +# First line should be in imperative mood; try rephrasing + D202, +# No blank lines allowed after function docstring + D203, +# 1 blank line required before class docstring + D213 +# Multi-line docstring summary should start at the second line +show-pep8 = True +show-source = True +count = True +max-line-length = 120