Migrate away from distutils

Python 3.12 has removed the `distutils` module, so we need to stop relying on
it. Most of the parts we were using had a straightforward replacement in
`setuptools` or elsewhere in the Python standard library. I couldn't find a
good replacement for `distutils.command.clean`, though. We were only using it
to enable `python setup.py clean`, so I just removed that functionality since
directly invoking setup.py is deprecated anyway.

While I was looking at this I realized that our python/release.sh script is
unused, so I also removed that.

PiperOrigin-RevId: 571035275
pull/14304/head
Adam Cozzette 1 year ago committed by Copybara-Service
parent 6cc34ca5aa
commit 8970072608
  1. 1
      python/build_targets.bzl
  2. 2
      python/dist/setup.py
  3. 205
      python/protobuf_distutils/protobuf_distutils/generate_py_protobufs.py
  4. 32
      python/protobuf_distutils/setup.py
  5. 137
      python/release.sh
  6. 36
      python/setup.py

@ -465,7 +465,6 @@ def build_targets(name):
"google/protobuf/python_protobuf.h",
"internal.bzl",
"python_version_test.py",
"release.sh",
"setup.cfg",
"setup.py",
"tox.ini",

@ -13,8 +13,6 @@ import os
import sys
import sysconfig
# We must use setuptools, not distutils, because we need to use the
# namespace_packages option for the "google" package.
from setuptools import setup, Extension, find_packages

@ -10,115 +10,128 @@
__author__ = 'dlj@google.com (David L. Jones)'
import glob
import sys
import os
import distutils.spawn as spawn
from distutils.cmd import Command
from distutils.errors import DistutilsOptionError, DistutilsExecError
import shutil
import subprocess
import sys
from setuptools import Command
from setuptools.errors import OptionError
class generate_py_protobufs(Command):
"""Generates Python sources for .proto files."""
"""Generates Python sources for .proto files."""
description = 'Generate Python sources for .proto files'
user_options = [
description = 'Generate Python sources for .proto files'
user_options = [
('extra-proto-paths=', None,
'Additional paths to resolve imports in .proto files.'),
('protoc=', None,
'Path to a specific `protoc` command to use.'),
]
boolean_options = ['recurse']
boolean_options = ['recurse']
def initialize_options(self):
"""Sets the defaults for the command options."""
self.source_dir = None
self.proto_root_path = None
self.extra_proto_paths = []
self.output_dir = '.'
self.proto_files = None
self.recurse = True
self.protoc = None
def initialize_options(self):
"""Sets the defaults for the command options."""
self.source_dir = None
self.proto_root_path = None
self.extra_proto_paths = []
self.output_dir = '.'
self.proto_files = None
self.recurse = True
self.protoc = None
def finalize_options(self):
"""Sets the final values for the command options.
def finalize_options(self):
"""Sets the final values for the command options.
Defaults were set in `initialize_options`, but could have been changed
by command-line options or by other commands.
"""
self.ensure_dirname('source_dir')
self.ensure_string_list('extra_proto_paths')
if self.output_dir is None:
self.output_dir = '.'
self.ensure_dirname('output_dir')
# SUBTLE: if 'source_dir' is a subdirectory of any entry in
# 'extra_proto_paths', then in general, the shortest --proto_path prefix
# (and the longest relative .proto filenames) must be used for
# correctness. For example, consider:
#
# source_dir = 'a/b/c'
# extra_proto_paths = ['a/b', 'x/y']
#
# In this case, we must ensure that a/b/c/d/foo.proto resolves
# canonically as c/d/foo.proto, not just d/foo.proto. Otherwise, this
# import:
#
# import "c/d/foo.proto";
#
# would result in different FileDescriptor.name keys from "d/foo.proto".
# That will cause all the definitions in the file to be flagged as
# duplicates, with an error similar to:
#
# c/d/foo.proto: "packagename.MessageName" is already defined in file "d/foo.proto"
#
# For paths in self.proto_files, we transform them to be relative to
# self.proto_root_path, which may be different from self.source_dir.
#
# Although the order of --proto_paths is significant, shadowed filenames
# are errors: if 'a/b/c.proto' resolves to different files under two
# different --proto_path arguments, then the path is rejected as an
# error. (Implementation note: this is enforced in protoc's
# DiskSourceTree class.)
if self.proto_root_path is None:
self.proto_root_path = os.path.normpath(self.source_dir)
for root_candidate in self.extra_proto_paths:
root_candidate = os.path.normpath(root_candidate)
if self.proto_root_path.startswith(root_candidate):
self.proto_root_path = root_candidate
if self.proto_root_path != self.source_dir:
self.announce('using computed proto_root_path: ' + self.proto_root_path, level=2)
if not self.source_dir.startswith(self.proto_root_path):
raise DistutilsOptionError('source_dir ' + self.source_dir +
' is not under proto_root_path ' + self.proto_root_path)
if self.proto_files is None:
files = glob.glob(os.path.join(self.source_dir, '*.proto'))
if self.recurse:
files.extend(glob.glob(os.path.join(self.source_dir, '**', '*.proto'), recursive=True))
self.proto_files = [f.partition(self.proto_root_path + os.path.sep)[-1] for f in files]
if not self.proto_files:
raise DistutilsOptionError('no .proto files were found under ' + self.source_dir)
self.ensure_string_list('proto_files')
if self.protoc is None:
self.protoc = os.getenv('PROTOC')
if self.protoc is None:
self.protoc = spawn.find_executable('protoc')
def run(self):
# All proto file paths were adjusted in finalize_options to be relative
# to self.proto_root_path.
proto_paths = ['--proto_path=' + self.proto_root_path]
proto_paths.extend(['--proto_path=' + x for x in self.extra_proto_paths])
# Run protoc. It was already resolved, so don't try to resolve
# through PATH.
spawn.spawn(
[self.protoc,
'--python_out=' + self.output_dir,
] + proto_paths + self.proto_files,
search_path=0)
self.ensure_dirname('source_dir')
self.ensure_string_list('extra_proto_paths')
if self.output_dir is None:
self.output_dir = '.'
self.ensure_dirname('output_dir')
# SUBTLE: if 'source_dir' is a subdirectory of any entry in
# 'extra_proto_paths', then in general, the shortest --proto_path prefix
# (and the longest relative .proto filenames) must be used for
# correctness. For example, consider:
#
# source_dir = 'a/b/c'
# extra_proto_paths = ['a/b', 'x/y']
#
# In this case, we must ensure that a/b/c/d/foo.proto resolves
# canonically as c/d/foo.proto, not just d/foo.proto. Otherwise, this
# import:
#
# import "c/d/foo.proto";
#
# would result in different FileDescriptor.name keys from "d/foo.proto".
# That will cause all the definitions in the file to be flagged as
# duplicates, with an error similar to:
#
# c/d/foo.proto: "packagename.MessageName" is already defined in file "d/foo.proto"
#
# For paths in self.proto_files, we transform them to be relative to
# self.proto_root_path, which may be different from self.source_dir.
#
# Although the order of --proto_paths is significant, shadowed filenames
# are errors: if 'a/b/c.proto' resolves to different files under two
# different --proto_path arguments, then the path is rejected as an
# error. (Implementation note: this is enforced in protoc's
# DiskSourceTree class.)
if self.proto_root_path is None:
self.proto_root_path = os.path.normpath(self.source_dir)
for root_candidate in self.extra_proto_paths:
root_candidate = os.path.normpath(root_candidate)
if self.proto_root_path.startswith(root_candidate):
self.proto_root_path = root_candidate
if self.proto_root_path != self.source_dir:
self.announce('using computed proto_root_path: ' + self.proto_root_path, level=2)
if not self.source_dir.startswith(self.proto_root_path):
raise OptionError(
'source_dir '
+ self.source_dir
+ ' is not under proto_root_path '
+ self.proto_root_path
)
if self.proto_files is None:
files = glob.glob(os.path.join(self.source_dir, '*.proto'))
if self.recurse:
files.extend(
glob.glob(
os.path.join(self.source_dir, '**', '*.proto'), recursive=True
)
)
self.proto_files = [
f.partition(self.proto_root_path + os.path.sep)[-1] for f in files
]
if not self.proto_files:
raise OptionError('no .proto files were found under ' + self.source_dir)
self.ensure_string_list('proto_files')
if self.protoc is None:
self.protoc = os.getenv('PROTOC')
if self.protoc is None:
self.protoc = shutil.which('protoc')
def run(self):
# All proto file paths were adjusted in finalize_options to be relative
# to self.proto_root_path.
proto_paths = ['--proto_path=' + self.proto_root_path]
proto_paths.extend(['--proto_path=' + x for x in self.extra_proto_paths])
# Run protoc.
subprocess.run(
[
self.protoc,
'--python_out=' + self.output_dir,
]
+ proto_paths
+ self.proto_files
)

@ -5,7 +5,7 @@
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
"""Setuptools/distutils extension for generating Python protobuf code."""
"""Setuptools extension for generating Python protobuf code."""
__author__ = 'dlj@google.com (David L. Jones)'
@ -25,26 +25,30 @@ setup(
maintainer_email='protobuf@googlegroups.com',
license='BSD-3-Clause',
classifiers=[
"Framework :: Setuptools Plugin",
"Operating System :: OS Independent",
'Framework :: Setuptools Plugin',
'Operating System :: OS Independent',
# These Python versions should match the protobuf package:
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Software Development :: Code Generators",
'Programming Language :: Python',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.8',
'Programming Language :: Python :: 3.9',
'Programming Language :: Python :: 3.10',
'Programming Language :: Python :: 3.11',
'Topic :: Software Development :: Code Generators',
],
description=('This is a distutils extension to generate Python code for '
'.proto files using an installed protoc binary.'),
description=(
'This is a setuptools extension to generate Python code for '
'.proto files using an installed protoc binary.'
),
long_description=_readme,
long_description_content_type='text/markdown',
url='https://github.com/protocolbuffers/protobuf/',
entry_points={
'distutils.commands': [
('generate_py_protobufs = '
'protobuf_distutils.generate_py_protobufs:generate_py_protobufs'),
(
'generate_py_protobufs = '
'protobuf_distutils.generate_py_protobufs:generate_py_protobufs'
),
],
},
)

@ -1,137 +0,0 @@
#!/bin/bash
set -ex
function get_source_version() {
grep "__version__ = '.*'" python/google/protobuf/__init__.py | sed -r "s/__version__ = '(.*)'/\1/"
}
function run_install_test() {
local VERSION=$1
local PYTHON=$2
local PYPI=$3
virtualenv -p `which $PYTHON` test-venv
# Intentionally put a broken protoc in the path to make sure installation
# doesn't require protoc installed.
touch test-venv/bin/protoc
chmod +x test-venv/bin/protoc
source test-venv/bin/activate
(pip install -i ${PYPI} protobuf==${VERSION} --no-cache-dir) || (retry_pip_install ${PYPI} ${VERSION})
deactivate
rm -fr test-venv
}
function retry_pip_install() {
local PYPI=$1
local VERSION=$2
read -p "pip install failed, possibly due to delay between upload and availability on pip. Retry? [y/n]" -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
(pip install -i ${PYPI} protobuf==${VERSION} --no-cache-dir) || (retry_pip_install ${PYPI} ${VERSION})
}
[ $# -lt 1 ] && {
echo "Usage: $0 VERSION ["
echo ""
echo "Examples:"
echo " Test 3.3.0 release using version number 3.3.0.dev1:"
echo " $0 3.0.0 dev1"
echo " Actually release 3.3.0 to PyPI:"
echo " $0 3.3.0"
exit 1
}
VERSION=$1
DEV=$2
# Make sure we are in a protobuf source tree.
[ -f "python/google/protobuf/__init__.py" ] || {
echo "This script must be ran under root of protobuf source tree."
exit 1
}
# Make sure all files are world-readable.
find python -type d -exec chmod a+r,a+x {} +
find python -type f -exec chmod a+r {} +
umask 0022
# Check that the supplied version number matches what's inside the source code.
SOURCE_VERSION=`get_source_version`
[ "${VERSION}" == "${SOURCE_VERSION}" -o "${VERSION}.${DEV}" == "${SOURCE_VERSION}" ] || {
echo "Version number specified on the command line ${VERSION} doesn't match"
echo "the actual version number in the source code: ${SOURCE_VERSION}"
exit 1
}
TESTING_ONLY=1
TESTING_VERSION=${VERSION}.${DEV}
if [ -z "${DEV}" ]; then
read -p "You are releasing ${VERSION} to PyPI. Are you sure? [y/n]" -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
TESTING_ONLY=0
TESTING_VERSION=${VERSION}
else
# Use dev version number for testing.
sed -i -r "s/__version__ = '.*'/__version__ = '${VERSION}.${DEV}'/" python/google/protobuf/__init__.py
fi
# Copy LICENSE
cp LICENSE python/LICENSE
cd python
# Run tests locally.
python3 setup.py build
python3 setup.py test
# Deploy source package to testing PyPI
python3 setup.py sdist
twine upload --skip-existing -r testpypi -u protobuf-wheel-test dist/*
# Sleep to allow time for distribution to be available on pip.
sleep 5m
# Test locally.
run_install_test ${TESTING_VERSION} python3 https://test.pypi.org/simple
# Deploy egg/wheel packages to testing PyPI and test again.
python3 setup.py clean build bdist_wheel
twine upload --skip-existing -r testpypi -u protobuf-wheel-test dist/*
sleep 5m
run_install_test ${TESTING_VERSION} python3 https://test.pypi.org/simple
echo "All install tests have passed using testing PyPI."
if [ $TESTING_ONLY -eq 0 ]; then
read -p "Publish to PyPI? [y/n]" -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
exit 1
fi
echo "Publishing to PyPI..."
# Be sure to run build before sdist, because otherwise sdist will not include
# well-known types.
python3 setup.py clean build sdist
twine upload --skip-existing -u protobuf-packages dist/*
# Be sure to run clean before bdist_xxx, because otherwise bdist_xxx will
# include files you may not want in the package. E.g., if you have built
# and tested with --cpp_implemenation, bdist_xxx will include the _message.so
# file even when you no longer pass the --cpp_implemenation flag. See:
# https://github.com/protocolbuffers/protobuf/issues/3042
python3 setup.py clean build bdist_wheel
twine upload --skip-existing -u protobuf-packages dist/*
else
# Set the version number back (i.e., remove dev suffix).
sed -i -r "s/__version__ = '.*'/__version__ = '${VERSION}'/" google/protobuf/__init__.py
fi

@ -10,12 +10,12 @@
# pylint:disable=missing-module-docstring
# pylint:disable=g-bad-import-order
from distutils import util
import fnmatch
import glob
import os
import pkg_resources
import re
import shutil
import subprocess
import sys
import sysconfig
@ -23,14 +23,10 @@ import sysconfig
# pylint:disable=g-importing-member
# pylint:disable=g-multiple-import
# We must use setuptools, not distutils, because we need to use the
# namespace_packages option for the "google" package.
from setuptools import setup, Extension, find_packages
from distutils.command.build_ext import build_ext as _build_ext
from distutils.command.build_py import build_py as _build_py
from distutils.command.clean import clean as _clean
from distutils.spawn import find_executable
from setuptools.command.build_ext import build_ext as _build_ext
from setuptools.command.build_py import build_py as _build_py
# Find the Protocol Compiler.
if 'PROTOC' in os.environ and os.path.exists(os.environ['PROTOC']):
@ -48,7 +44,7 @@ elif os.path.exists('../vsprojects/Debug/protoc.exe'):
elif os.path.exists('../vsprojects/Release/protoc.exe'):
protoc = '../vsprojects/Release/protoc.exe'
else:
protoc = find_executable('protoc')
protoc = shutil.which('protoc')
def GetVersion():
@ -144,21 +140,6 @@ def GenerateUnittestProtos():
GenProto('google/protobuf/pyext/python.proto', False)
class CleanCmd(_clean):
"""Custom clean command for building the protobuf extension."""
def run(self):
# Delete generated files in the code tree.
for (dirpath, unused_dirnames, filenames) in os.walk('.'):
for filename in filenames:
filepath = os.path.join(dirpath, filename)
if (filepath.endswith('_pb2.py') or filepath.endswith('.pyc') or
filepath.endswith('.so') or filepath.endswith('.o')):
os.remove(filepath)
# _clean is an old-style class, so super() doesn't work.
_clean.run(self)
class BuildPyCmd(_build_py):
"""Custom build_py command for building the protobuf runtime."""
@ -231,7 +212,7 @@ def GetOptionFromArgv(option_str):
def _GetFlagValues(flag_long, flag_short):
"""Searches sys.argv for distutils-style flags and yields values."""
"""Searches sys.argv for setuptools-style flags and yields values."""
expect_value = flag_long.endswith('=')
flag_res = [re.compile(r'--?%s(=(.*))?' %
@ -362,8 +343,10 @@ if __name__ == '__main__':
pkg_resources.parse_version('10.9.0')):
os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.9'
os.environ['_PYTHON_HOST_PLATFORM'] = re.sub(
r'macosx-[0-9]+\.[0-9]+-(.+)', r'macosx-10.9-\1',
util.get_platform())
r'macosx-[0-9]+\.[0-9]+-(.+)',
r'macosx-10.9-\1',
sysconfig.get_platform(),
)
# https://github.com/Theano/Theano/issues/4926
if sys.platform == 'win32':
@ -442,7 +425,6 @@ if __name__ == '__main__':
),
test_suite='google.protobuf.internal',
cmdclass={
'clean': CleanCmd,
'build_py': BuildPyCmd,
'build_ext': BuildExtCmd,
'test_conformance': TestConformanceCmd,

Loading…
Cancel
Save