From f9398b1f2bd6cce0aa4efa01d8b69da021205151 Mon Sep 17 00:00:00 2001 From: Avasam Date: Thu, 8 Aug 2024 14:20:53 -0400 Subject: [PATCH] Mark abstract base classes and methods (#4503) * Mark abstract base classes and methods --- newsfragments/4503.feature.rst | 1 + pkg_resources/__init__.py | 3 +- setuptools/__init__.py | 65 ++++++++++++++++++++-------------- setuptools/command/setopt.py | 7 +++- setuptools/sandbox.py | 3 +- 5 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 newsfragments/4503.feature.rst diff --git a/newsfragments/4503.feature.rst b/newsfragments/4503.feature.rst new file mode 100644 index 0000000000..9c2e433242 --- /dev/null +++ b/newsfragments/4503.feature.rst @@ -0,0 +1 @@ +Mark abstract base classes and methods with `abc.ABC` and `abc.abstractmethod` -- by :user:`Avasam` diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index a87fdf8570..f4206c493d 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -22,6 +22,7 @@ from __future__ import annotations +from abc import ABC import sys if sys.version_info < (3, 8): # noqa: UP036 # Check for unsupported versions @@ -311,7 +312,7 @@ def get_supported_platform(): ] -class ResolutionError(Exception): +class ResolutionError(Exception, ABC): """Abstract base for dependency resolution errors""" def __repr__(self): diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 69c1f5acb9..84294edb31 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations +from abc import ABC, abstractmethod import functools import os import re @@ -119,7 +120,7 @@ def setup(**attrs): _Command = monkey.get_unpatched(distutils.core.Command) -class Command(_Command): +class Command(_Command, ABC): """ Setuptools internal actions are organized using a *command design pattern*. This means that each action (or group of closely related actions) executed during @@ -132,42 +133,25 @@ class Command(_Command): When creating a new command from scratch, custom defined classes **SHOULD** inherit from ``setuptools.Command`` and implement a few mandatory methods. Between these mandatory methods, are listed: - - .. method:: initialize_options(self) - - Set or (reset) all options/attributes/caches used by the command - to their default values. Note that these values may be overwritten during - the build. - - .. method:: finalize_options(self) - - Set final values for all options/attributes used by the command. - Most of the time, each option/attribute/cache should only be set if it does not - have any value yet (e.g. ``if self.attr is None: self.attr = val``). - - .. method:: run(self) - - Execute the actions intended by the command. - (Side effects **SHOULD** only take place when ``run`` is executed, - for example, creating new files or writing to the terminal output). + :meth:`initialize_options`, :meth:`finalize_options` and :meth:`run`. A useful analogy for command classes is to think of them as subroutines with local - variables called "options". The options are "declared" in ``initialize_options()`` - and "defined" (given their final values, aka "finalized") in ``finalize_options()``, + variables called "options". The options are "declared" in :meth:`initialize_options` + and "defined" (given their final values, aka "finalized") in :meth:`finalize_options`, both of which must be defined by every command class. The "body" of the subroutine, - (where it does all the work) is the ``run()`` method. - Between ``initialize_options()`` and ``finalize_options()``, ``setuptools`` may set + (where it does all the work) is the :meth:`run` method. + Between :meth:`initialize_options` and :meth:`finalize_options`, ``setuptools`` may set the values for options/attributes based on user's input (or circumstance), which means that the implementation should be careful to not overwrite values in - ``finalize_options`` unless necessary. + :meth:`finalize_options` unless necessary. Please note that other commands (or other parts of setuptools) may also overwrite the values of the command's options/attributes multiple times during the build process. - Therefore it is important to consistently implement ``initialize_options()`` and - ``finalize_options()``. For example, all derived attributes (or attributes that + Therefore it is important to consistently implement :meth:`initialize_options` and + :meth:`finalize_options`. For example, all derived attributes (or attributes that depend on the value of other attributes) **SHOULD** be recomputed in - ``finalize_options``. + :meth:`finalize_options`. When overwriting existing commands, custom defined classes **MUST** abide by the same APIs implemented by the original class. They also **SHOULD** inherit from the @@ -238,6 +222,33 @@ def reinitialize_command( vars(cmd).update(kw) return cmd + @abstractmethod + def initialize_options(self) -> None: + """ + Set or (reset) all options/attributes/caches used by the command + to their default values. Note that these values may be overwritten during + the build. + """ + raise NotImplementedError + + @abstractmethod + def finalize_options(self) -> None: + """ + Set final values for all options/attributes used by the command. + Most of the time, each option/attribute/cache should only be set if it does not + have any value yet (e.g. ``if self.attr is None: self.attr = val``). + """ + raise NotImplementedError + + @abstractmethod + def run(self) -> None: + """ + Execute the actions intended by the command. + (Side effects **SHOULD** only take place when :meth:`run` is executed, + for example, creating new files or writing to the terminal output). + """ + raise NotImplementedError + def _find_all_simple(path): """ diff --git a/setuptools/command/setopt.py b/setuptools/command/setopt.py index b78d845e60..0cd67c0f67 100644 --- a/setuptools/command/setopt.py +++ b/setuptools/command/setopt.py @@ -1,3 +1,4 @@ +from abc import ABC, abstractmethod from distutils.util import convert_path from distutils import log from distutils.errors import DistutilsOptionError @@ -68,7 +69,7 @@ def edit_config(filename, settings, dry_run=False): opts.write(f) -class option_base(Command): +class option_base(Command, ABC): """Abstract base class for commands that mess with config files""" user_options = [ @@ -103,6 +104,10 @@ def finalize_options(self): ) (self.filename,) = filenames + @abstractmethod + def run(self) -> None: + raise NotImplementedError + class setopt(option_base): """Save command-line options to a file""" diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 147b26749e..31ba1e3f8d 100644 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -1,5 +1,6 @@ from __future__ import annotations +from abc import ABC import os import sys import tempfile @@ -265,7 +266,7 @@ def run_setup(setup_script, args): # Normal exit, just return -class AbstractSandbox: +class AbstractSandbox(ABC): """Wrap 'os' module and 'open()' builtin for virtualizing setup scripts""" _active = False