diff --git a/.gitignore b/.gitignore index 90ae80505e4..f25d073e243 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # syntax: glob +# See https://blog.jaraco.com/skeleton/#ignoring-artifacts before modifying. bin build dist @@ -17,6 +18,5 @@ setuptools.egg-info *~ .hg* .cache -.idea/ .pytest_cache/ .mypy_cache/ diff --git a/docs/conf.py b/docs/conf.py index 0a82ff2fe2c..be8856849b3 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -161,22 +161,23 @@ # Ref: https://stackoverflow.com/a/30624034/595220 nitpick_ignore = [ ('c:func', 'SHGetSpecialFolderPath'), # ref to MS docs + ('envvar', 'DIST_EXTRA_CONFIG'), # undocumented ('envvar', 'DISTUTILS_DEBUG'), # undocumented ('envvar', 'HOME'), # undocumented ('envvar', 'PLAT'), # undocumented - ('envvar', 'DIST_EXTRA_CONFIG'), # undocumented ('py:attr', 'CCompiler.language_map'), # undocumented ('py:attr', 'CCompiler.language_order'), # undocumented - ('py:class', 'distutils.dist.Distribution'), # undocumented - ('py:class', 'distutils.extension.Extension'), # undocumented ('py:class', 'BorlandCCompiler'), # undocumented ('py:class', 'CCompiler'), # undocumented ('py:class', 'CygwinCCompiler'), # undocumented + ('py:class', 'distutils.dist.Distribution'), # undocumented ('py:class', 'distutils.dist.DistributionMetadata'), # undocumented + ('py:class', 'distutils.extension.Extension'), # undocumented ('py:class', 'FileList'), # undocumented ('py:class', 'IShellLink'), # ref to MS docs ('py:class', 'MSVCCompiler'), # undocumented ('py:class', 'OptionDummy'), # undocumented + ('py:class', 'setuptools.dist.Distribution'), # undocumented ('py:class', 'UnixCCompiler'), # undocumented ('py:exc', 'CompileError'), # undocumented ('py:exc', 'DistutilsExecError'), # undocumented @@ -186,8 +187,7 @@ ('py:exc', 'PreprocessError'), # undocumented ('py:exc', 'setuptools.errors.PlatformError'), # sphinx cannot find it ('py:func', 'distutils.CCompiler.new_compiler'), # undocumented - # undocumented: - ('py:func', 'distutils.dist.DistributionMetadata.read_pkg_file'), + ('py:func', 'distutils.dist.DistributionMetadata.read_pkg_file'), # undocumented ('py:func', 'distutils.file_util._copy_file_contents'), # undocumented ('py:func', 'distutils.log.debug'), # undocumented ('py:func', 'distutils.spawn.find_executable'), # undocumented diff --git a/docs/userguide/datafiles.rst b/docs/userguide/datafiles.rst index 2e37289d5f8..4eca7e43033 100644 --- a/docs/userguide/datafiles.rst +++ b/docs/userguide/datafiles.rst @@ -10,12 +10,19 @@ by including the data files **inside the package directory**. Setuptools focuses on this most common type of data files and offers three ways of specifying which files should be included in your packages, as described in -the following sections. +the following section. + + +Configuration Options +===================== + + +.. _include-package-data: include_package_data -==================== +-------------------- -First, you can simply use the ``include_package_data`` keyword. +First, you can use the ``include_package_data`` keyword. For example, if the package tree looks like this:: project_root_directory @@ -92,8 +99,10 @@ your package, provided: (where ``include_package_data=False`` by default), which was not changed to ensure backwards compatibility with existing projects. +.. _package-data: + package_data -============ +------------ By default, ``include_package_data`` considers **all** non ``.py`` files found inside the package directory (``src/mypkg`` in this case) as data files, and includes those that @@ -260,8 +269,10 @@ we specify that ``data1.rst`` from ``mypkg1`` alone should be captured as well. Please check :ref:`section subdirectories ` below. +.. _exclude-package-data: + exclude_package_data -==================== +-------------------- Sometimes, the ``include_package_data`` or ``package_data`` options alone aren't sufficient to precisely define what files you want included. For example, @@ -327,6 +338,38 @@ even if they were listed in ``package_data`` or were included as a result of usi ``include_package_data``. +Summary +------- + +In summary, the three options allow you to: + +``include_package_data`` + Accept all data files and directories matched by + :ref:`MANIFEST.in ` or added by + a :ref:`plugin `. + +``package_data`` + Specify additional patterns to match files that may or may + not be matched by :ref:`MANIFEST.in ` + or added by a :ref:`plugin `. + +``exclude_package_data`` + Specify patterns for data files and directories that should *not* be + included when a package is installed, even if they would otherwise have + been included due to the use of the preceding options. + +.. note:: + Due to the way the build process works, a data file that you + include in your project and then stop including may be "orphaned" in your + project's build directories, requiring you to manually deleting them. + This may also be important for your users and contributors + if they track intermediate revisions of your project using Subversion; be sure + to let them know when you make changes that remove files from inclusion so they + can also manually delete them. + + See also troubleshooting information in :ref:`Caching and Troubleshooting`. + + .. _subdir-data-files: Subdirectory for Data Files @@ -350,8 +393,13 @@ Here, the ``.rst`` files are placed under a ``data`` subdirectory inside ``mypkg while the ``.txt`` files are directly under ``mypkg``. In this case, the recommended approach is to treat ``data`` as a namespace package -(refer :pep:`420`). With ``package_data``, -the configuration might look like this: +(refer :pep:`420`). This way, you can rely on the same methods described above, +using either :ref:`package-data` or :ref:`include-package-data`. +For the sake of completeness, we include below configuration examples +for the subdirectory structure, but please refer to the detailed +information in the previous sections of this document. + +With :ref:`package-data`, the configuration might look like this: .. tab:: pyproject.toml @@ -407,8 +455,9 @@ which enables the ``data`` directory to be identified, and then, we separately s files for the root package ``mypkg``, and the namespace package ``data`` under the package ``mypkg``. -With ``include_package_data`` the configuration is simpler: you simply need to enable -scanning of namespace packages in the ``src`` directory and the rest is handled by Setuptools. +Alternatively, you can also rely on :ref:`include-package-data`. +Note that this is the default behaviour in ``pyproject.toml``, but you need to +manually enable scanning of namespace packages in ``setup.cfg`` or ``setup.py``: .. tab:: pyproject.toml @@ -422,7 +471,7 @@ scanning of namespace packages in the ``src`` directory and the rest is handled [tool.setuptools.packages.find] # scanning for namespace packages is true by default in pyproject.toml, so - # you need NOT include the following line. + # you need NOT include this configuration. namespaces = true where = ["src"] @@ -451,34 +500,9 @@ scanning of namespace packages in the ``src`` directory and the rest is handled include_package_data=True, ) -Summary -======= - -In summary, the three options allow you to: - -``include_package_data`` - Accept all data files and directories matched by - :ref:`MANIFEST.in ` or added by - a :ref:`plugin `. - -``package_data`` - Specify additional patterns to match files that may or may - not be matched by :ref:`MANIFEST.in ` - or added by a :ref:`plugin `. - -``exclude_package_data`` - Specify patterns for data files and directories that should *not* be - included when a package is installed, even if they would otherwise have - been included due to the use of the preceding options. - -.. note:: - Due to the way the build process works, a data file that you - include in your project and then stop including may be "orphaned" in your - project's build directories, requiring you to manually deleting them. - This may also be important for your users and contributors - if they track intermediate revisions of your project using Subversion; be sure - to let them know when you make changes that remove files from inclusion so they - can also manually delete them. +To avoid common mistakes with :ref:`include-package-data`, +please ensure :ref:`MANIFEST.in ` is properly set +or use a revision control system plugin (see :doc:`/userguide/miscellaneous`). .. _Accessing Data Files at Runtime: diff --git a/docs/userguide/dependency_management.rst b/docs/userguide/dependency_management.rst index 0feb346dc54..a2c0c890f33 100644 --- a/docs/userguide/dependency_management.rst +++ b/docs/userguide/dependency_management.rst @@ -19,7 +19,7 @@ Build system requirement After organizing all the scripts and files and getting ready for packaging, there needs to be a way to specify what programs and libraries are actually needed -do the packaging (in our case, ``setuptools`` of course). +to do the packaging (in our case, ``setuptools`` of course). This needs to be specified in your ``pyproject.toml`` file (if you have forgot what this is, go to :doc:`/userguide/quickstart` or :doc:`/build_meta`): diff --git a/docs/userguide/miscellaneous.rst b/docs/userguide/miscellaneous.rst index 5f15ff6053d..7d841f66617 100644 --- a/docs/userguide/miscellaneous.rst +++ b/docs/userguide/miscellaneous.rst @@ -168,6 +168,20 @@ binary extensions during the build process, or included in the final See :doc:`/userguide/datafiles` for more information. + +.. _Caching and Troubleshooting: + +Caching and Troubleshooting +=========================== + +Setuptools automatically creates a few directories to host build artefacts and +cache files, such as ``build``, ``dist``, ``*.egg-info``. While cache is +useful to speed up incremental builds, in some edge cases it might become +stale. If you feel that caching is causing problems to your build, specially +after changes in configuration or in the directory/file structure., consider +removing ``build``, ``dist``, ``*.egg-info`` [#PKG-INFO]_ before rebuilding or +reinstalling your project. + ---- .. [#build-process] @@ -183,5 +197,9 @@ binary extensions during the build process, or included in the final :term:`Virtual Environment`. Therefore it only contains items that are required during runtime. +.. [#PKG-INFO] + When working from an extracted sdist (e.g. for patching), you might also consider removing + the ``PKG-INFO`` file to force its recreation. + .. _git: https://git-scm.com .. _mercurial: https://www.mercurial-scm.org diff --git a/mypy.ini b/mypy.ini index b6f972769e6..42ade6537ee 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,33 @@ [mypy] -ignore_missing_imports = True -# required to support namespace packages -# https://github.com/python/mypy/issues/14057 +# CI should test for all versions, local development gets hints for oldest supported +python_version = 3.8 +strict = False +warn_unused_ignores = True +# required to support namespace packages: https://github.com/python/mypy/issues/14057 explicit_package_bases = True +exclude = (?x)( + ^build/ + | ^.tox/ + | ^pkg_resources/tests/data/my-test-package-source/setup.py$ # Duplicate module name + | ^.+?/(_vendor|extern)/ # Vendored + | ^setuptools/_distutils/ # Vendored + | ^setuptools/config/_validate_pyproject/ # Auto-generated + ) +# Ignoring attr-defined because setuptools wraps a lot of distutils classes, adding new attributes, +# w/o updating all the attributes and return types from the base classes for type-checkers to understand +# Especially with setuptools.dist.command vs distutils.dist.command vs setuptools._distutils.dist.command +# *.extern modules that actually live in *._vendor will also cause attr-defined issues on import +disable_error_code = attr-defined + +# - Avoid raising issues when importing from "extern" modules, as those are added to path dynamically. +# https://github.com/pypa/setuptools/pull/3979#discussion_r1367968993 +# - distutils._modified has different errors on Python 3.8 [import-untyped], on Python 3.9+ [import-not-found] +# - All jaraco modules are still untyped +[mypy-pkg_resources.extern.*,setuptools.extern.*,distutils._modified,jaraco.*] +ignore_missing_imports = True + +# - pkg_resources tests create modules that won't exists statically before the test is run. +# Let's ignore all "import-not-found" since, if an import really wasn't found, then the test would fail. +# - setuptools._vendor.packaging._manylinux: Mypy issue, this vendored module is already excluded! +[mypy-pkg_resources.tests.*,setuptools._vendor.packaging._manylinux] +disable_error_code = import-not-found diff --git a/newsfragments/4243.bugfix.rst b/newsfragments/4243.bugfix.rst new file mode 100644 index 00000000000..e8212721f3e --- /dev/null +++ b/newsfragments/4243.bugfix.rst @@ -0,0 +1 @@ +Clarify some `pkg_resources` methods return `bytes`, not `str`. Also return an empty `bytes` in ``EmptyProvider._get`` -- by :user:`Avasam` diff --git a/newsfragments/4244.bugfix.rst b/newsfragments/4244.bugfix.rst new file mode 100644 index 00000000000..5d606de718b --- /dev/null +++ b/newsfragments/4244.bugfix.rst @@ -0,0 +1 @@ +Return an empty `list` by default in ``pkg_resources.ResourceManager.cleanup_resources`` -- by :user:`Avasam` diff --git a/newsfragments/4254.bugfix.rst b/newsfragments/4254.bugfix.rst new file mode 100644 index 00000000000..e944fcfb496 --- /dev/null +++ b/newsfragments/4254.bugfix.rst @@ -0,0 +1 @@ +Made ``pkg_resoursces.NullProvider``'s ``has_metadata`` and ``metadata_isdir`` methods return actual booleans like all other Providers. -- by :user:`Avasam` diff --git a/newsfragments/4260.misc.rst b/newsfragments/4260.misc.rst new file mode 100644 index 00000000000..9dfde3498d2 --- /dev/null +++ b/newsfragments/4260.misc.rst @@ -0,0 +1 @@ +Remove unused ``resources_stream`` ``resource_dir`` and shadowed functions from `pkg_resources` -- by :user:`Avasam` diff --git a/newsfragments/4261.misc.rst b/newsfragments/4261.misc.rst new file mode 100644 index 00000000000..83c10f0f66a --- /dev/null +++ b/newsfragments/4261.misc.rst @@ -0,0 +1 @@ +Avoid implicit ``encoding`` parameter in ``setuptools/tests``. diff --git a/newsfragments/4263.misc.rst b/newsfragments/4263.misc.rst new file mode 100644 index 00000000000..f84eb8dd426 --- /dev/null +++ b/newsfragments/4263.misc.rst @@ -0,0 +1 @@ +Avoid implicit ``encoding`` parameter in ``pkg_resources/tests``. diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 10c6a9cd060..163a5521d67 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -27,7 +27,7 @@ import time import re import types -from typing import Protocol +from typing import List, Protocol import zipfile import zipimport import warnings @@ -85,9 +85,7 @@ require = None working_set = None add_activation_listener = None -resources_stream = None cleanup_resources = None -resource_dir = None resource_stream = None set_extraction_path = None resource_isdir = None @@ -491,19 +489,6 @@ def compatible_platforms(provided, required): return False -def run_script(dist_spec, script_name): - """Locate distribution `dist_spec` and run its `script_name` script""" - ns = sys._getframe(1).f_globals - name = ns['__name__'] - ns.clear() - ns['__name__'] = name - require(dist_spec)[0].run_script(script_name, ns) - - -# backward compatibility -run_main = run_script - - def get_distribution(dist): """Return a current distribution object for a Requirement or string""" if isinstance(dist, str): @@ -531,7 +516,7 @@ def get_entry_info(dist, group, name): class IMetadataProvider(Protocol): - def has_metadata(self, name): + def has_metadata(self, name) -> bool: """Does the package's distribution contain the named metadata?""" def get_metadata(self, name): @@ -543,7 +528,7 @@ def get_metadata_lines(self, name): Leading and trailing whitespace is stripped from each line, and lines with ``#`` as the first non-blank character are omitted.""" - def metadata_isdir(self, name): + def metadata_isdir(self, name) -> bool: """Is the named metadata a directory? (like ``os.path.isdir()``)""" def metadata_listdir(self, name): @@ -566,8 +551,8 @@ def get_resource_stream(self, manager, resource_name): `manager` must be an ``IResourceManager``""" - def get_resource_string(self, manager, resource_name): - """Return a string containing the contents of `resource_name` + def get_resource_string(self, manager, resource_name) -> bytes: + """Return the contents of `resource_name` as :obj:`bytes` `manager` must be an ``IResourceManager``""" @@ -1203,8 +1188,8 @@ def resource_stream(self, package_or_requirement, resource_name): self, resource_name ) - def resource_string(self, package_or_requirement, resource_name): - """Return specified resource as a string""" + def resource_string(self, package_or_requirement, resource_name) -> bytes: + """Return specified resource as :obj:`bytes`""" return get_provider(package_or_requirement).get_resource_string( self, resource_name ) @@ -1339,7 +1324,7 @@ def set_extraction_path(self, path): self.extraction_path = path - def cleanup_resources(self, force=False): + def cleanup_resources(self, force=False) -> List[str]: """ Delete all extracted resource files and directories, returning a list of the file and directory names that could not be successfully removed. @@ -1351,6 +1336,7 @@ def cleanup_resources(self, force=False): directory used for extractions. """ # XXX + return [] def get_default_cache(): @@ -1479,7 +1465,7 @@ def get_resource_filename(self, manager, resource_name): def get_resource_stream(self, manager, resource_name): return io.BytesIO(self.get_resource_string(manager, resource_name)) - def get_resource_string(self, manager, resource_name): + def get_resource_string(self, manager, resource_name) -> bytes: return self._get(self._fn(self.module_path, resource_name)) def has_resource(self, resource_name): @@ -1488,9 +1474,9 @@ def has_resource(self, resource_name): def _get_metadata_path(self, name): return self._fn(self.egg_info, name) - def has_metadata(self, name): + def has_metadata(self, name) -> bool: if not self.egg_info: - return self.egg_info + return False path = self._get_metadata_path(name) return self._has(path) @@ -1514,8 +1500,8 @@ def get_metadata_lines(self, name): def resource_isdir(self, resource_name): return self._isdir(self._fn(self.module_path, resource_name)) - def metadata_isdir(self, name): - return self.egg_info and self._isdir(self._fn(self.egg_info, name)) + def metadata_isdir(self, name) -> bool: + return bool(self.egg_info and self._isdir(self._fn(self.egg_info, name))) def resource_listdir(self, resource_name): return self._listdir(self._fn(self.module_path, resource_name)) @@ -1554,12 +1540,12 @@ def run_script(self, script_name, namespace): script_code = compile(script_text, script_filename, 'exec') exec(script_code, namespace, namespace) - def _has(self, path): + def _has(self, path) -> bool: raise NotImplementedError( "Can't perform this operation for unregistered loader type" ) - def _isdir(self, path): + def _isdir(self, path) -> bool: raise NotImplementedError( "Can't perform this operation for unregistered loader type" ) @@ -1649,7 +1635,7 @@ def _validate_resource_path(path): DeprecationWarning, ) - def _get(self, path): + def _get(self, path) -> bytes: if hasattr(self.loader, 'get_data'): return self.loader.get_data(path) raise NotImplementedError( @@ -1694,10 +1680,10 @@ def _set_egg(self, path): class DefaultProvider(EggProvider): """Provides access to package resources in the filesystem""" - def _has(self, path): + def _has(self, path) -> bool: return os.path.exists(path) - def _isdir(self, path): + def _isdir(self, path) -> bool: return os.path.isdir(path) def _listdir(self, path): @@ -1706,7 +1692,7 @@ def _listdir(self, path): def get_resource_stream(self, manager, resource_name): return open(self._fn(self.module_path, resource_name), 'rb') - def _get(self, path): + def _get(self, path) -> bytes: with open(path, 'rb') as stream: return stream.read() @@ -1731,8 +1717,8 @@ class EmptyProvider(NullProvider): _isdir = _has = lambda self, path: False - def _get(self, path): - return '' + def _get(self, path) -> bytes: + return b'' def _listdir(self, path): return [] @@ -1939,11 +1925,11 @@ def _index(self): self._dirindex = ind return ind - def _has(self, fspath): + def _has(self, fspath) -> bool: zip_path = self._zipinfo_name(fspath) return zip_path in self.zipinfo or zip_path in self._index() - def _isdir(self, fspath): + def _isdir(self, fspath) -> bool: return self._zipinfo_name(fspath) in self._index() def _listdir(self, fspath): @@ -1977,7 +1963,7 @@ def __init__(self, path): def _get_metadata_path(self, name): return self.path - def has_metadata(self, name): + def has_metadata(self, name) -> bool: return name == 'PKG-INFO' and os.path.isfile(self.path) def get_metadata(self, name): @@ -3207,7 +3193,9 @@ def _find_adapter(registry, ob): for t in types: if t in registry: return registry[t] - return None + # _find_adapter would previously return None, and immediatly be called. + # So we're raising a TypeError to keep backward compatibility if anyone depended on that behaviour. + raise TypeError(f"Could not find adapter for {registry} and {ob}") def ensure_directory(path): diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index 08836420808..4724c828605 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -9,6 +9,7 @@ import stat import distutils.dist import distutils.command.install_egg_info +from typing import List from unittest import mock @@ -32,7 +33,7 @@ def __call__(self): class TestZipProvider: - finalizers = [] + finalizers: List[EggRemover] = [] ref_time = datetime.datetime(2013, 5, 12, 13, 25, 0) "A reference time for a file modification" @@ -110,13 +111,13 @@ def test_resource_filename_rewrites_on_change(self): filename = zp.get_resource_filename(manager, 'data.dat') actual = datetime.datetime.fromtimestamp(os.stat(filename).st_mtime) assert actual == self.ref_time - f = open(filename, 'w') + f = open(filename, 'w', encoding="utf-8") f.write('hello, world?') f.close() ts = self.ref_time.timestamp() os.utime(filename, (ts, ts)) filename = zp.get_resource_filename(manager, 'data.dat') - with open(filename) as f: + with open(filename, encoding="utf-8") as f: assert f.read() == 'hello, world!' manager.cleanup_resources() diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index 5b2308aea71..b0a319e60f2 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -35,7 +35,7 @@ class Metadata(pkg_resources.EmptyProvider): def __init__(self, *pairs): self.metadata = dict(pairs) - def has_metadata(self, name): + def has_metadata(self, name) -> bool: return name in self.metadata def get_metadata(self, name): diff --git a/pyproject.toml b/pyproject.toml index cd95aad07f8..58aacd9fe39 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,3 @@ build-backend = "setuptools.build_meta" backend-path = ["."] [tool.setuptools_scm] - -[tool.pytest-enabler.mypy] -# disabled diff --git a/setup.cfg b/setup.cfg index 91fb67724c6..4d1155e884d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -73,6 +73,10 @@ testing = # for tools/finalize.py jaraco.develop >= 7.21; python_version >= "3.9" and sys_platform != "cygwin" pytest-home >= 0.5 + # No Python 3.11 dependencies require tomli, but needed for type-checking since we import it directly + tomli + # No Python 3.12 dependencies require importlib_metadata, but needed for type-checking since we import it directly + importlib_metadata testing-integration = pytest diff --git a/setup.py b/setup.py index 075d7c405fc..1a6074766a5 100755 --- a/setup.py +++ b/setup.py @@ -88,5 +88,6 @@ def _restore_install_lib(self): if __name__ == '__main__': # allow setup.py to run from another directory - here and os.chdir(here) + # TODO: Use a proper conditonal statement here + here and os.chdir(here) # type: ignore[func-returns-value] dist = setuptools.setup(**setup_params) diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 563ca1c4baf..7c88c7e19b2 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -3,6 +3,7 @@ import functools import os import re +from typing import TYPE_CHECKING import _distutils_hack.override # noqa: F401 import distutils.core @@ -105,8 +106,11 @@ def setup(**attrs): setup.__doc__ = distutils.core.setup.__doc__ - -_Command = monkey.get_unpatched(distutils.core.Command) +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + _Command = distutils.core.Command +else: + _Command = monkey.get_unpatched(distutils.core.Command) class Command(_Command): @@ -165,8 +169,9 @@ class Command(_Command): """ command_consumes_arguments = False + distribution: Distribution # override distutils.dist.Distribution with setuptools.dist.Distribution - def __init__(self, dist, **kw): + def __init__(self, dist: Distribution, **kw): """ Construct the command for dist, updating vars(self) with any keyword parameters. diff --git a/setuptools/command/_requirestxt.py b/setuptools/command/_requirestxt.py index 7b732b11abf..b0c2d7059ad 100644 --- a/setuptools/command/_requirestxt.py +++ b/setuptools/command/_requirestxt.py @@ -35,7 +35,7 @@ def _prepare( def _convert_extras_requirements( - extras_require: _StrOrIter, + extras_require: Mapping[str, _StrOrIter], ) -> Mapping[str, _Ordered[Requirement]]: """ Convert requirements in `extras_require` of the form diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py index 780afe3aece..b5c98c86dcd 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -16,7 +16,7 @@ try: # Attempt to use Cython for building extensions, if available - from Cython.Distutils.build_ext import build_ext as _build_ext + from Cython.Distutils.build_ext import build_ext as _build_ext # type: ignore[import-not-found] # Cython not installed on CI tests # Additionally, assert that the compiler module will load # also. Ref #1229. @@ -26,7 +26,9 @@ # make sure _config_vars is initialized get_config_var("LDSHARED") -from distutils.sysconfig import _config_vars as _CONFIG_VARS # noqa +# Not publicly exposed in typeshed distutils stubs, but this is done on purpose +# See https://github.com/pypa/setuptools/pull/4228#issuecomment-1959856400 +from distutils.sysconfig import _config_vars as _CONFIG_VARS # type: ignore # noqa def _customize_compiler_for_shlib(compiler): @@ -58,7 +60,7 @@ def _customize_compiler_for_shlib(compiler): use_stubs = True elif os.name != 'nt': try: - import dl + import dl # type: ignore[import-not-found] # https://github.com/python/mypy/issues/13002 use_stubs = have_rtld = hasattr(dl, 'RTLD_NOW') except ImportError: @@ -378,7 +380,10 @@ def _compile_and_remove_stub(self, stub_file: str): optimize = self.get_finalized_command('install_lib').optimize if optimize > 0: byte_compile( - [stub_file], optimize=optimize, force=True, dry_run=self.dry_run + [stub_file], + optimize=optimize, + force=True, + dry_run=self.dry_run, ) if os.path.exists(stub_file) and not self.dry_run: os.unlink(stub_file) diff --git a/setuptools/command/dist_info.py b/setuptools/command/dist_info.py index f5061afaaf7..52c0721903d 100644 --- a/setuptools/command/dist_info.py +++ b/setuptools/command/dist_info.py @@ -9,8 +9,10 @@ from distutils import log from distutils.core import Command from pathlib import Path +from typing import cast from .. import _normalization +from .egg_info import egg_info as egg_info_cls class dist_info(Command): @@ -50,7 +52,7 @@ def finalize_options(self): project_dir = dist.src_root or os.curdir self.output_dir = Path(self.output_dir or project_dir) - egg_info = self.reinitialize_command("egg_info") + egg_info = cast(egg_info_cls, self.reinitialize_command("egg_info")) egg_info.egg_base = str(self.output_dir) if self.tag_date: diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index cc0c4091231..402355bd816 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -25,6 +25,7 @@ from distutils.command import install import sys import os +from typing import Dict, List import zipimport import shutil import tempfile @@ -43,7 +44,6 @@ import configparser import sysconfig - from sysconfig import get_path from setuptools import Command @@ -1765,7 +1765,7 @@ def _wrap_lines(cls, lines): if os.environ.get('SETUPTOOLS_SYS_PATH_TECHNIQUE', 'raw') == 'rewrite': - PthDistributions = RewritePthDistributions + PthDistributions = RewritePthDistributions # type: ignore[misc] # Overwriting type def _first_line_re(): @@ -2015,7 +2015,7 @@ def is_python_script(script_text, filename): from os import chmod as _chmod except ImportError: # Jython compatibility - def _chmod(*args): + def _chmod(*args: object, **kwargs: object) -> None: # type: ignore[misc] # Mypy re-uses the imported definition anyway pass @@ -2033,8 +2033,8 @@ class CommandSpec(list): those passed to Popen. """ - options = [] - split_args = dict() + options: List[str] = [] + split_args: Dict[str, bool] = dict() @classmethod def best(cls): diff --git a/setuptools/command/editable_wheel.py b/setuptools/command/editable_wheel.py index ec41847c2f0..5cb115f8acf 100644 --- a/setuptools/command/editable_wheel.py +++ b/setuptools/command/editable_wheel.py @@ -33,6 +33,7 @@ Protocol, Tuple, TypeVar, + cast, ) from .. import ( @@ -50,10 +51,15 @@ SetuptoolsDeprecationWarning, SetuptoolsWarning, ) +from .build import build as build_cls from .build_py import build_py as build_py_cls +from .dist_info import dist_info as dist_info_cls +from .egg_info import egg_info as egg_info_cls +from .install import install as install_cls +from .install_scripts import install_scripts as install_scripts_cls if TYPE_CHECKING: - from wheel.wheelfile import WheelFile # noqa + from wheel.wheelfile import WheelFile # type:ignore[import-untyped] # noqa _P = TypeVar("_P", bound=StrPath) _logger = logging.getLogger(__name__) @@ -155,7 +161,7 @@ def run(self): def _ensure_dist_info(self): if self.dist_info_dir is None: - dist_info = self.reinitialize_command("dist_info") + dist_info = cast(dist_info_cls, self.reinitialize_command("dist_info")) dist_info.output_dir = self.dist_dir dist_info.ensure_finalized() dist_info.run() @@ -202,12 +208,18 @@ def _configure_build( scripts = str(Path(unpacked_wheel, f"{name}.data", "scripts")) # egg-info may be generated again to create a manifest (used for package data) - egg_info = dist.reinitialize_command("egg_info", reinit_subcommands=True) + egg_info = cast( + egg_info_cls, dist.reinitialize_command("egg_info", reinit_subcommands=True) + ) egg_info.egg_base = str(tmp_dir) egg_info.ignore_egg_info_in_manifest = True - build = dist.reinitialize_command("build", reinit_subcommands=True) - install = dist.reinitialize_command("install", reinit_subcommands=True) + build = cast( + build_cls, dist.reinitialize_command("build", reinit_subcommands=True) + ) + install = cast( + install_cls, dist.reinitialize_command("install", reinit_subcommands=True) + ) build.build_platlib = build.build_purelib = build.build_lib = build_lib install.install_purelib = install.install_platlib = install.install_lib = wheel @@ -215,12 +227,14 @@ def _configure_build( install.install_headers = headers install.install_data = data - install_scripts = dist.get_command_obj("install_scripts") + install_scripts = cast( + install_scripts_cls, dist.get_command_obj("install_scripts") + ) install_scripts.no_ep = True build.build_temp = str(tmp_dir) - build_py = dist.get_command_obj("build_py") + build_py = cast(build_py_cls, dist.get_command_obj("build_py")) build_py.compile = False build_py.existing_egg_info_dir = self._find_egg_info_dir() @@ -233,6 +247,7 @@ def _set_editable_mode(self): """Set the ``editable_mode`` flag in the build sub-commands""" dist = self.distribution build = dist.get_command_obj("build") + # TODO: Update typeshed distutils stubs to overload non-None return type by default for cmd_name in build.get_sub_commands(): cmd = dist.get_command_obj(cmd_name) if hasattr(cmd, "editable_mode"): @@ -269,7 +284,7 @@ def _run_build_commands( self._run_install("data") return files, mapping - def _run_build_subcommands(self): + def _run_build_subcommands(self) -> None: """ Issue #3501 indicates that some plugins/customizations might rely on: @@ -283,7 +298,7 @@ def _run_build_subcommands(self): # TODO: Once plugins/customisations had the chance to catch up, replace # `self._run_build_subcommands()` with `self.run_command("build")`. # Also remove _safely_run, TestCustomBuildPy. Suggested date: Aug/2023. - build: Command = self.get_finalized_command("build") + build = self.get_finalized_command("build") for name in build.get_sub_commands(): cmd = self.get_finalized_command(name) if name == "build_py" and type(cmd) != build_py_cls: @@ -432,7 +447,8 @@ def __init__( ): self.auxiliary_dir = Path(auxiliary_dir) self.build_lib = Path(build_lib).resolve() - self._file = dist.get_command_obj("build_py").copy_file + # TODO: Update typeshed distutils stubs to overload non-None return type by default + self._file = dist.get_command_obj("build_py").copy_file # type: ignore[union-attr] super().__init__(dist, name, [self.auxiliary_dir]) def __call__(self, wheel: "WheelFile", files: List[str], mapping: Dict[str, str]): @@ -450,7 +466,9 @@ def _create_file(self, relative_output: str, src_file: str, link=None): dest = self.auxiliary_dir / relative_output if not dest.parent.is_dir(): dest.parent.mkdir(parents=True) - self._file(src_file, dest, link=link) + # TODO: Update typeshed distutils stubs so distutils.cmd.Command.copy_file, accepts PathLike + # same with methods used by copy_file + self._file(src_file, dest, link=link) # type: ignore[arg-type] def _create_links(self, outputs, output_mapping): self.auxiliary_dir.mkdir(parents=True, exist_ok=True) @@ -604,7 +622,8 @@ def _simple_layout( layout = {pkg: find_package_path(pkg, package_dir, project_dir) for pkg in packages} if not layout: return set(package_dir) in ({}, {""}) - parent = os.path.commonpath(starmap(_parent_path, layout.items())) + # TODO: has been fixed upstream, waiting for new mypy release https://github.com/python/typeshed/pull/11310 + parent = os.path.commonpath(starmap(_parent_path, layout.items())) # type: ignore[call-overload] return all( _path.same_path(Path(parent, *key.split('.')), value) for key, value in layout.items() diff --git a/setuptools/command/install.py b/setuptools/command/install.py index b97a9b4713f..56c1155b503 100644 --- a/setuptools/command/install.py +++ b/setuptools/command/install.py @@ -3,9 +3,11 @@ import glob import platform import distutils.command.install as orig +from typing import cast import setuptools from ..warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning +from .bdist_egg import bdist_egg as bdist_egg_cls # Prior to numpy 1.9, NumPy relies on the '_install' name, so provide it for # now. See https://github.com/pypa/setuptools/issues/199/ @@ -135,7 +137,8 @@ def do_egg_install(self): cmd.package_index.scan(glob.glob('*.egg')) self.run_command('bdist_egg') - args = [self.distribution.get_command_obj('bdist_egg').egg_output] + bdist_egg = cast(bdist_egg_cls, self.distribution.get_command_obj('bdist_egg')) + args = [bdist_egg.egg_output] if setuptools.bootstrap_install_from: # Bootstrap self-installation of setuptools diff --git a/setuptools/command/rotate.py b/setuptools/command/rotate.py index cfb78ce52d2..6f73721c700 100644 --- a/setuptools/command/rotate.py +++ b/setuptools/command/rotate.py @@ -3,6 +3,7 @@ from distutils.errors import DistutilsOptionError import os import shutil +from typing import List from setuptools import Command @@ -17,7 +18,7 @@ class rotate(Command): ('keep=', 'k', "number of matching distributions to keep"), ] - boolean_options = [] + boolean_options: List[str] = [] def initialize_options(self): self.match = None diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py index 32c9abd796f..3fbbb62553f 100644 --- a/setuptools/command/upload_docs.py +++ b/setuptools/command/upload_docs.py @@ -50,7 +50,7 @@ def has_sphinx(self): and metadata.entry_points(group='distutils.commands', name='build_sphinx') ) - sub_commands = [('build_sphinx', has_sphinx)] + sub_commands = [('build_sphinx', has_sphinx)] # type: ignore[list-item] # TODO: Fix in typeshed distutils stubs def initialize_options(self): upload.initialize_options(self) diff --git a/setuptools/config/_apply_pyprojecttoml.py b/setuptools/config/_apply_pyprojecttoml.py index 7301bc65c13..3626282a791 100644 --- a/setuptools/config/_apply_pyprojecttoml.py +++ b/setuptools/config/_apply_pyprojecttoml.py @@ -34,6 +34,7 @@ from ..warnings import SetuptoolsWarning if TYPE_CHECKING: + from distutils.dist import _OptionsList from setuptools._importlib import metadata # noqa from setuptools.dist import Distribution # noqa @@ -293,7 +294,7 @@ def _normalise_cmd_option_key(name: str) -> str: return json_compatible_key(name).strip("_=") -def _normalise_cmd_options(desc: List[Tuple[str, Optional[str], str]]) -> Set[str]: +def _normalise_cmd_options(desc: "_OptionsList") -> Set[str]: return {_normalise_cmd_option_key(fancy_option[0]) for fancy_option in desc} diff --git a/setuptools/config/_validate_pyproject/fastjsonschema_validations.py b/setuptools/config/_validate_pyproject/fastjsonschema_validations.py index b81d13c1190..8b852bbfd41 100644 --- a/setuptools/config/_validate_pyproject/fastjsonschema_validations.py +++ b/setuptools/config/_validate_pyproject/fastjsonschema_validations.py @@ -1,5 +1,4 @@ # noqa -# type: ignore # flake8: noqa # pylint: skip-file # mypy: ignore-errors diff --git a/setuptools/config/expand.py b/setuptools/config/expand.py index e23a762cf58..0d8d58add82 100644 --- a/setuptools/config/expand.py +++ b/setuptools/config/expand.py @@ -63,7 +63,7 @@ class StaticModule: """Proxy to a module object that avoids executing arbitrary code.""" def __init__(self, name: str, spec: ModuleSpec): - module = ast.parse(pathlib.Path(spec.origin).read_bytes()) + module = ast.parse(pathlib.Path(spec.origin).read_bytes()) # type: ignore[arg-type] # Let it raise an error on None vars(self).update(locals()) del self.self diff --git a/setuptools/config/pyprojecttoml.py b/setuptools/config/pyprojecttoml.py index 5eb9421f1fe..ff97679895f 100644 --- a/setuptools/config/pyprojecttoml.py +++ b/setuptools/config/pyprojecttoml.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: from setuptools.dist import Distribution # noqa + from typing_extensions import Self _logger = logging.getLogger(__name__) @@ -271,7 +272,7 @@ def _ensure_previously_set(self, dist: "Distribution", field: str): def _expand_directive( self, specifier: str, directive, package_dir: Mapping[str, str] ): - from setuptools.extern.more_itertools import always_iterable # type: ignore + from setuptools.extern.more_itertools import always_iterable with _ignore_errors(self.ignore_option_errors): root_dir = self.root_dir @@ -401,7 +402,7 @@ def __init__( self._project_cfg = project_cfg self._setuptools_cfg = setuptools_cfg - def __enter__(self): + def __enter__(self) -> "Self": """When entering the context, the values of ``packages``, ``py_modules`` and ``package_dir`` that are missing in ``dist`` are copied from ``setuptools_cfg``. """ diff --git a/setuptools/config/setupcfg.py b/setuptools/config/setupcfg.py index cfa43a57b56..2912d3e1439 100644 --- a/setuptools/config/setupcfg.py +++ b/setuptools/config/setupcfg.py @@ -108,7 +108,7 @@ def _apply( filenames = [*other_files, filepath] try: - _Distribution.parse_config_files(dist, filenames=filenames) + _Distribution.parse_config_files(dist, filenames=filenames) # type: ignore[arg-type] # TODO: fix in disutils stubs handlers = parse_configuration( dist, dist.command_options, ignore_option_errors=ignore_option_errors ) @@ -475,7 +475,7 @@ def parse_section(self, section_options): # Keep silent for a new option may appear anytime. self[name] = value - def parse(self): + def parse(self) -> None: """Parses configuration file items from one or more related sections. diff --git a/setuptools/dist.py b/setuptools/dist.py index d5787ed4745..6350e381006 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -10,7 +10,7 @@ from contextlib import suppress from glob import iglob from pathlib import Path -from typing import List, Optional, Set +from typing import TYPE_CHECKING, Dict, List, MutableMapping, Optional, Set, Tuple import distutils.cmd import distutils.command @@ -202,7 +202,11 @@ def check_packages(dist, attr, value): ) -_Distribution = get_unpatched(distutils.core.Distribution) +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + _Distribution = distutils.core.Distribution +else: + _Distribution = get_unpatched(distutils.core.Distribution) class Distribution(_Distribution): @@ -283,12 +287,12 @@ def patch_missing_pkg_info(self, attrs): dist._version = _normalization.safe_version(str(attrs['version'])) self._patched_dist = dist - def __init__(self, attrs=None): + def __init__(self, attrs: Optional[MutableMapping] = None) -> None: have_package_data = hasattr(self, "package_data") if not have_package_data: - self.package_data = {} + self.package_data: Dict[str, List[str]] = {} attrs = attrs or {} - self.dist_files = [] + self.dist_files: List[Tuple[str, str, str]] = [] # Filter-out setuptools' specific options. self.src_root = attrs.pop("src_root", None) self.patch_missing_pkg_info(attrs) @@ -381,7 +385,7 @@ def _normalize_requires(self): k: list(map(str, _reqs.parse(v or []))) for k, v in extras_require.items() } - def _finalize_license_files(self): + def _finalize_license_files(self) -> None: """Compute names of all license files which should be included.""" license_files: Optional[List[str]] = self.metadata.license_files patterns: List[str] = license_files if license_files else [] @@ -394,7 +398,7 @@ def _finalize_license_files(self): # Default patterns match the ones wheel uses # See https://wheel.readthedocs.io/en/stable/user_guide.html # -> 'Including license files in the generated wheel file' - patterns = ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*') + patterns = ['LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*'] self.metadata.license_files = list( unique_everseen(self._expand_patterns(patterns)) diff --git a/setuptools/extension.py b/setuptools/extension.py index 58c023f6b44..8caad78d4b1 100644 --- a/setuptools/extension.py +++ b/setuptools/extension.py @@ -3,6 +3,7 @@ import distutils.core import distutils.errors import distutils.extension +from typing import TYPE_CHECKING from .monkey import get_unpatched @@ -23,8 +24,11 @@ def _have_cython(): # for compatibility have_pyrex = _have_cython - -_Extension = get_unpatched(distutils.core.Extension) +if TYPE_CHECKING: + # Work around a mypy issue where type[T] can't be used as a base: https://github.com/python/mypy/issues/10962 + _Extension = distutils.core.Extension +else: + _Extension = get_unpatched(distutils.core.Extension) class Extension(_Extension): diff --git a/setuptools/monkey.py b/setuptools/monkey.py index da0993506c3..fd07d91dece 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -8,11 +8,14 @@ import sys import types from importlib import import_module +from typing import List, TypeVar import distutils.filelist -__all__ = [] +_T = TypeVar("_T") + +__all__: List[str] = [] """ Everything is private. Contact the project team if you think you need this functionality. @@ -33,7 +36,7 @@ def _get_mro(cls): return inspect.getmro(cls) -def get_unpatched(item): +def get_unpatched(item: _T) -> _T: lookup = ( get_unpatched_class if isinstance(item, type) diff --git a/setuptools/msvc.py b/setuptools/msvc.py index 53fe7b0de14..b2a0f2bebbf 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -20,9 +20,11 @@ import itertools import subprocess import distutils.errors +from typing import Dict, TYPE_CHECKING from setuptools.extern.more_itertools import unique_everseen -if platform.system() == 'Windows': +# https://github.com/python/mypy/issues/8166 +if not TYPE_CHECKING and platform.system() == 'Windows': import winreg from os import environ else: @@ -34,7 +36,7 @@ class winreg: HKEY_LOCAL_MACHINE = None HKEY_CLASSES_ROOT = None - environ = dict() + environ: Dict[str, str] = dict() def _msvc14_find_vc2015(): diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 7634b1320b1..6c095e029e4 100644 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -9,6 +9,7 @@ import pickle import textwrap import builtins +from typing import Union, List import pkg_resources from distutils.errors import DistutilsError @@ -19,7 +20,7 @@ else: _os = sys.modules[os.name] try: - _file = file + _file = file # type: ignore[name-defined] # Check for global variable except NameError: _file = None _open = open @@ -298,7 +299,7 @@ def run(self, func): with self: return func() - def _mk_dual_path_wrapper(name): + def _mk_dual_path_wrapper(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = getattr(_os, name) def wrap(self, src, dst, *args, **kw): @@ -312,7 +313,7 @@ def wrap(self, src, dst, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_dual_path_wrapper(name) - def _mk_single_path_wrapper(name, original=None): + def _mk_single_path_wrapper(name: str, original=None): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = original or getattr(_os, name) def wrap(self, path, *args, **kw): @@ -349,7 +350,7 @@ def wrap(self, path, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_single_path_wrapper(name) - def _mk_single_with_return(name): + def _mk_single_with_return(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = getattr(_os, name) def wrap(self, path, *args, **kw): @@ -364,7 +365,7 @@ def wrap(self, path, *args, **kw): if hasattr(_os, name): locals()[name] = _mk_single_with_return(name) - def _mk_query(name): + def _mk_query(name: str): # type: ignore[misc] # https://github.com/pypa/setuptools/pull/4099 original = getattr(_os, name) def wrap(self, *args, **kw): @@ -424,7 +425,7 @@ class DirectorySandbox(AbstractSandbox): "tempnam", ]) - _exception_patterns = [] + _exception_patterns: List[Union[str, re.Pattern]] = [] "exempt writing to paths that match the pattern" def __init__(self, sandbox, exceptions=_EXCEPTIONS): diff --git a/setuptools/tests/config/test_apply_pyprojecttoml.py b/setuptools/tests/config/test_apply_pyprojecttoml.py index e63a89b0b4e..2ca35759bc8 100644 --- a/setuptools/tests/config/test_apply_pyprojecttoml.py +++ b/setuptools/tests/config/test_apply_pyprojecttoml.py @@ -9,6 +9,7 @@ import tarfile from inspect import cleandoc from pathlib import Path +from typing import Tuple from unittest.mock import Mock from zipfile import ZipFile @@ -43,8 +44,9 @@ def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path): monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1")) setupcfg_example = retrieve_file(url) pyproject_example = Path(tmp_path, "pyproject.toml") - toml_config = Translator().translate(setupcfg_example.read_text(), "setup.cfg") - pyproject_example.write_text(toml_config) + setupcfg_text = setupcfg_example.read_text(encoding="utf-8") + toml_config = Translator().translate(setupcfg_text, "setup.cfg") + pyproject_example.write_text(toml_config, encoding="utf-8") dist_toml = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject_example) dist_cfg = setupcfg.apply_configuration(makedist(tmp_path), setupcfg_example) @@ -176,9 +178,9 @@ def _pep621_example_project( text = text.replace(orig, subst) pyproject.write_text(text, encoding="utf-8") - (tmp_path / readme).write_text("hello world") - (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---") - (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT) + (tmp_path / readme).write_text("hello world", encoding="utf-8") + (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---", encoding="utf-8") + (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT, encoding="utf-8") return pyproject @@ -455,7 +457,7 @@ def core_metadata(dist) -> str: # Make sure core metadata is valid Metadata.from_email(pkg_file_txt, validate=True) # can raise exceptions - skip_prefixes = () + skip_prefixes: Tuple[str, ...] = () skip_lines = set() # ---- DIFF NORMALISATION ---- # PEP 621 is very particular about author/maintainer metadata conversion, so skip diff --git a/setuptools/tests/config/test_expand.py b/setuptools/tests/config/test_expand.py index cdcbffc14c5..fe80890678c 100644 --- a/setuptools/tests/config/test_expand.py +++ b/setuptools/tests/config/test_expand.py @@ -12,7 +12,7 @@ def write_files(files, root_dir): for file, content in files.items(): path = root_dir / file path.parent.mkdir(exist_ok=True, parents=True) - path.write_text(content) + path.write_text(content, encoding="utf-8") def test_glob_relative(tmp_path, monkeypatch): diff --git a/setuptools/tests/config/test_pyprojecttoml.py b/setuptools/tests/config/test_pyprojecttoml.py index 6a40f3bfd79..abec68ab306 100644 --- a/setuptools/tests/config/test_pyprojecttoml.py +++ b/setuptools/tests/config/test_pyprojecttoml.py @@ -2,6 +2,7 @@ from configparser import ConfigParser from inspect import cleandoc +import jaraco.path import pytest import tomli_w from path import Path @@ -82,25 +83,32 @@ def create_example(path, pkg_root): - pyproject = path / "pyproject.toml" - - files = [ - f"{pkg_root}/pkg/__init__.py", - "_files/file.txt", - ] - if pkg_root != ".": # flat-layout will raise error for multi-package dist - # Ensure namespaces are discovered - files.append(f"{pkg_root}/other/nested/__init__.py") + files = { + "pyproject.toml": EXAMPLE, + "README.md": "hello world", + "_files": { + "file.txt": "", + }, + } + packages = { + "pkg": { + "__init__.py": "", + "mod.py": "class CustomSdist: pass", + "__version__.py": "VERSION = (3, 10)", + "__main__.py": "def exec(): print('hello')", + }, + } + + assert pkg_root # Meta-test: cannot be empty string. - for file in files: - (path / file).parent.mkdir(exist_ok=True, parents=True) - (path / file).touch() + if pkg_root == ".": + files = {**files, **packages} + # skip other files: flat-layout will raise error for multi-package dist + else: + # Use this opportunity to ensure namespaces are discovered + files[pkg_root] = {**packages, "other": {"nested": {"__init__.py": ""}}} - pyproject.write_text(EXAMPLE) - (path / "README.md").write_text("hello world") - (path / f"{pkg_root}/pkg/mod.py").write_text("class CustomSdist: pass") - (path / f"{pkg_root}/pkg/__version__.py").write_text("VERSION = (3, 10)") - (path / f"{pkg_root}/pkg/__main__.py").write_text("def exec(): print('hello')") + jaraco.path.build(files, prefix=path) def verify_example(config, path, pkg_root): @@ -174,7 +182,7 @@ class TestEntryPoints: def write_entry_points(self, tmp_path): entry_points = ConfigParser() entry_points.read_dict(ENTRY_POINTS) - with open(tmp_path / "entry-points.txt", "w") as f: + with open(tmp_path / "entry-points.txt", "w", encoding="utf-8") as f: entry_points.write(f) def pyproject(self, dynamic=None): @@ -208,11 +216,13 @@ def test_dynamic(self, tmp_path): # Let's create a project example that has dynamic classifiers # coming from a txt file. create_example(tmp_path, "src") - classifiers = """\ - Framework :: Flask - Programming Language :: Haskell - """ - (tmp_path / "classifiers.txt").write_text(cleandoc(classifiers)) + classifiers = cleandoc( + """ + Framework :: Flask + Programming Language :: Haskell + """ + ) + (tmp_path / "classifiers.txt").write_text(classifiers, encoding="utf-8") pyproject = tmp_path / "pyproject.toml" config = read_configuration(pyproject, expand=False) @@ -240,7 +250,7 @@ def test_dynamic_without_config(self, tmp_path): """ pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(config)) + pyproject.write_text(cleandoc(config), encoding="utf-8") with pytest.raises(OptionError, match="No configuration .* .classifiers."): read_configuration(pyproject) @@ -252,7 +262,7 @@ def test_dynamic_readme_from_setup_script_args(self, tmp_path): dynamic = ["readme"] """ pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(config)) + pyproject.write_text(cleandoc(config), encoding="utf-8") dist = Distribution(attrs={"long_description": "42"}) # No error should occur because of missing `readme` dist = apply_configuration(dist, pyproject) @@ -270,7 +280,7 @@ def test_dynamic_without_file(self, tmp_path): """ pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(config)) + pyproject.write_text(cleandoc(config), encoding="utf-8") with pytest.warns(UserWarning, match="File .*classifiers.txt. cannot be found"): expanded = read_configuration(pyproject) assert "classifiers" not in expanded["project"] @@ -291,7 +301,7 @@ def test_dynamic_without_file(self, tmp_path): ) def test_ignore_unrelated_config(tmp_path, example): pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(example)) + pyproject.write_text(cleandoc(example), encoding="utf-8") # Make sure no error is raised due to 3rd party configs in pyproject.toml assert read_configuration(pyproject) is not None @@ -313,7 +323,7 @@ def test_ignore_unrelated_config(tmp_path, example): ) def test_invalid_example(tmp_path, example, error_msg): pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(cleandoc(example)) + pyproject.write_text(cleandoc(example), encoding="utf-8") pattern = re.compile(f"invalid pyproject.toml.*{error_msg}.*", re.M | re.S) with pytest.raises(ValueError, match=pattern): @@ -323,7 +333,7 @@ def test_invalid_example(tmp_path, example, error_msg): @pytest.mark.parametrize("config", ("", "[tool.something]\nvalue = 42")) def test_empty(tmp_path, config): pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(config) + pyproject.write_text(config, encoding="utf-8") # Make sure no error is raised assert read_configuration(pyproject) == {} @@ -335,7 +345,7 @@ def test_include_package_data_by_default(tmp_path, config): default. """ pyproject = tmp_path / "pyproject.toml" - pyproject.write_text(config) + pyproject.write_text(config, encoding="utf-8") config = read_configuration(pyproject) assert config["tool"]["setuptools"]["include-package-data"] is True @@ -347,10 +357,11 @@ def test_include_package_data_in_setuppy(tmp_path): See https://github.com/pypa/setuptools/issues/3197#issuecomment-1079023889 """ - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text("[project]\nname = 'myproj'\nversion='42'\n") - setuppy = tmp_path / "setup.py" - setuppy.write_text("__import__('setuptools').setup(include_package_data=False)") + files = { + "pyproject.toml": "[project]\nname = 'myproj'\nversion='42'\n", + "setup.py": "__import__('setuptools').setup(include_package_data=False)", + } + jaraco.path.build(files, prefix=tmp_path) with Path(tmp_path): dist = distutils.core.run_setup("setup.py", {}, stop_after="config") diff --git a/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py b/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py index b6516227c09..37e5234a450 100644 --- a/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py +++ b/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py @@ -1,57 +1,59 @@ +from inspect import cleandoc + import pytest +from jaraco import path from setuptools.config.pyprojecttoml import apply_configuration from setuptools.dist import Distribution -from setuptools.tests.textwrap import DALS def test_dynamic_dependencies(tmp_path): - (tmp_path / "requirements.txt").write_text("six\n # comment\n") - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text( - DALS( + files = { + "requirements.txt": "six\n # comment\n", + "pyproject.toml": cleandoc( """ - [project] - name = "myproj" - version = "1.0" - dynamic = ["dependencies"] + [project] + name = "myproj" + version = "1.0" + dynamic = ["dependencies"] - [build-system] - requires = ["setuptools", "wheel"] - build-backend = "setuptools.build_meta" + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" - [tool.setuptools.dynamic.dependencies] - file = ["requirements.txt"] - """ - ) - ) + [tool.setuptools.dynamic.dependencies] + file = ["requirements.txt"] + """ + ), + } + path.build(files, prefix=tmp_path) dist = Distribution() - dist = apply_configuration(dist, pyproject) + dist = apply_configuration(dist, tmp_path / "pyproject.toml") assert dist.install_requires == ["six"] def test_dynamic_optional_dependencies(tmp_path): - (tmp_path / "requirements-docs.txt").write_text("sphinx\n # comment\n") - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text( - DALS( + files = { + "requirements-docs.txt": "sphinx\n # comment\n", + "pyproject.toml": cleandoc( """ - [project] - name = "myproj" - version = "1.0" - dynamic = ["optional-dependencies"] + [project] + name = "myproj" + version = "1.0" + dynamic = ["optional-dependencies"] - [tool.setuptools.dynamic.optional-dependencies.docs] - file = ["requirements-docs.txt"] + [tool.setuptools.dynamic.optional-dependencies.docs] + file = ["requirements-docs.txt"] - [build-system] - requires = ["setuptools", "wheel"] - build-backend = "setuptools.build_meta" - """ - ) - ) + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + """ + ), + } + path.build(files, prefix=tmp_path) dist = Distribution() - dist = apply_configuration(dist, pyproject) + dist = apply_configuration(dist, tmp_path / "pyproject.toml") assert dist.extras_require == {"docs": ["sphinx"]} @@ -61,29 +63,32 @@ def test_mixed_dynamic_optional_dependencies(tmp_path): configurations in the case of fields containing sub-fields (groups), things would work out. """ - (tmp_path / "requirements-images.txt").write_text("pillow~=42.0\n # comment\n") - pyproject = tmp_path / "pyproject.toml" - pyproject.write_text( - DALS( + files = { + "requirements-images.txt": "pillow~=42.0\n # comment\n", + "pyproject.toml": cleandoc( """ - [project] - name = "myproj" - version = "1.0" - dynamic = ["optional-dependencies"] + [project] + name = "myproj" + version = "1.0" + dynamic = ["optional-dependencies"] - [project.optional-dependencies] - docs = ["sphinx"] + [project.optional-dependencies] + docs = ["sphinx"] - [tool.setuptools.dynamic.optional-dependencies.images] - file = ["requirements-images.txt"] + [tool.setuptools.dynamic.optional-dependencies.images] + file = ["requirements-images.txt"] + + [build-system] + requires = ["setuptools", "wheel"] + build-backend = "setuptools.build_meta" + """ + ), + } + + path.build(files, prefix=tmp_path) - [build-system] - requires = ["setuptools", "wheel"] - build-backend = "setuptools.build_meta" - """ - ) - ) # Test that the mix-and-match doesn't currently validate. + pyproject = tmp_path / "pyproject.toml" with pytest.raises(ValueError, match="project.optional-dependencies"): apply_configuration(Distribution(), pyproject) diff --git a/setuptools/tests/config/test_setupcfg.py b/setuptools/tests/config/test_setupcfg.py index 7f93858bd4d..706e2d0ebef 100644 --- a/setuptools/tests/config/test_setupcfg.py +++ b/setuptools/tests/config/test_setupcfg.py @@ -904,7 +904,8 @@ def test_cmdclass(self, tmpdir): module_path = Path(tmpdir, "src/custom_build.py") # auto discovery for src module_path.parent.mkdir(parents=True, exist_ok=True) module_path.write_text( - "from distutils.core import Command\n" "class CustomCmd(Command): pass\n" + "from distutils.core import Command\n" "class CustomCmd(Command): pass\n", + encoding="utf-8", ) setup_cfg = """ diff --git a/setuptools/tests/integration/helpers.py b/setuptools/tests/integration/helpers.py index 82cb36a2e46..615c43b2e0a 100644 --- a/setuptools/tests/integration/helpers.py +++ b/setuptools/tests/integration/helpers.py @@ -17,6 +17,7 @@ def run(cmd, env=None): cmd, capture_output=True, text=True, + encoding="utf-8", env={**os.environ, **(env or {})}, # ^-- allow overwriting instead of discarding the current env ) diff --git a/setuptools/tests/integration/test_pip_install_sdist.py b/setuptools/tests/integration/test_pip_install_sdist.py index 3467a5ec075..17bf2af9d24 100644 --- a/setuptools/tests/integration/test_pip_install_sdist.py +++ b/setuptools/tests/integration/test_pip_install_sdist.py @@ -1,3 +1,5 @@ +# https://github.com/python/mypy/issues/16936 +# mypy: disable-error-code="has-type" """Integration tests for setuptools that focus on building packages via pip. The idea behind these tests is not to exhaustively check all the possible @@ -25,10 +27,10 @@ from .helpers import Archive, run - pytestmark = pytest.mark.integration -(LATEST,) = Enum("v", "LATEST") + +(LATEST,) = Enum("v", "LATEST") # type: ignore[misc] # https://github.com/python/mypy/issues/16936 """Default version to be checked""" # There are positive and negative aspects of checking the latest version of the # packages. diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py index 0e473d168bd..12ed4d328c6 100644 --- a/setuptools/tests/test_bdist_egg.py +++ b/setuptools/tests/test_bdist_egg.py @@ -47,7 +47,7 @@ def test_bdist_egg(self, setup_context, user_override): assert re.match(r'foo-0.0.0-py[23].\d+.egg$', content) @pytest.mark.xfail( - os.environ.get('PYTHONDONTWRITEBYTECODE'), + os.environ.get('PYTHONDONTWRITEBYTECODE', False), reason="Byte code disabled", ) def test_exclude_source_files(self, setup_context, user_override): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 9332781764e..c2a1e6dc751 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -272,14 +272,14 @@ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): [metadata] name = foo version = file: VERSION - """ + """ ), 'pyproject.toml': DALS( """ [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" - """ + """ ), } @@ -296,7 +296,7 @@ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): first_result = build_method(dist_dir) # Change version. - with open("VERSION", "wt") as version_file: + with open("VERSION", "wt", encoding="utf-8") as version_file: version_file.write("0.0.2") # Build a *second* sdist/wheel. @@ -306,7 +306,7 @@ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd): assert first_result != second_result # And if rebuilding the exact same sdist/wheel? - open(os.path.join(dist_dir, second_result), 'w').close() + open(os.path.join(dist_dir, second_result), 'wb').close() third_result = build_method(dist_dir) assert third_result == second_result assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0 @@ -568,9 +568,9 @@ def test_build_sdist_version_change(self, build_backend): if not os.path.exists(setup_loc): setup_loc = os.path.abspath("setup.cfg") - with open(setup_loc, 'rt') as file_handler: + with open(setup_loc, 'rt', encoding="utf-8") as file_handler: content = file_handler.read() - with open(setup_loc, 'wt') as file_handler: + with open(setup_loc, 'wt', encoding="utf-8") as file_handler: file_handler.write(content.replace("version='0.0.0'", "version='0.0.1'")) shutil.rmtree(sdist_into_directory) diff --git a/setuptools/tests/test_build_py.py b/setuptools/tests/test_build_py.py index 500a9ab6f36..4aa1fe68faa 100644 --- a/setuptools/tests/test_build_py.py +++ b/setuptools/tests/test_build_py.py @@ -49,7 +49,7 @@ def test_recursive_in_package_data_glob(tmpdir_cwd): ) ) os.makedirs('path/subpath/subsubpath') - open('path/subpath/subsubpath/data', 'w').close() + open('path/subpath/subsubpath/data', 'wb').close() dist.parse_command_line() dist.run_commands() @@ -77,8 +77,8 @@ def test_read_only(tmpdir_cwd): ) ) os.makedirs('pkg') - open('pkg/__init__.py', 'w').close() - open('pkg/data.dat', 'w').close() + open('pkg/__init__.py', 'wb').close() + open('pkg/data.dat', 'wb').close() os.chmod('pkg/__init__.py', stat.S_IREAD) os.chmod('pkg/data.dat', stat.S_IREAD) dist.parse_command_line() @@ -108,8 +108,8 @@ def test_executable_data(tmpdir_cwd): ) ) os.makedirs('pkg') - open('pkg/__init__.py', 'w').close() - open('pkg/run-me', 'w').close() + open('pkg/__init__.py', 'wb').close() + open('pkg/run-me', 'wb').close() os.chmod('pkg/run-me', 0o700) dist.parse_command_line() diff --git a/setuptools/tests/test_config_discovery.py b/setuptools/tests/test_config_discovery.py index ef2979d4f5f..72772caebfe 100644 --- a/setuptools/tests/test_config_discovery.py +++ b/setuptools/tests/test_config_discovery.py @@ -176,11 +176,11 @@ def test_purposefully_empty(self, tmp_path, config_file, param, circumstance): else: # Make sure build works with or without setup.cfg pyproject = self.PURPOSEFULLY_EMPY["template-pyproject.toml"] - (tmp_path / "pyproject.toml").write_text(pyproject) + (tmp_path / "pyproject.toml").write_text(pyproject, encoding="utf-8") template_param = param config = self.PURPOSEFULLY_EMPY[config_file].format(param=template_param) - (tmp_path / config_file).write_text(config) + (tmp_path / config_file).write_text(config, encoding="utf-8") dist = _get_dist(tmp_path, {}) # When either parameter package or py_modules is an empty list, @@ -292,11 +292,13 @@ class TestWithAttrDirective: def test_setupcfg_metadata(self, tmp_path, folder, opts): files = [f"{folder}/pkg/__init__.py", "setup.cfg"] _populate_project_dir(tmp_path, files, opts) - (tmp_path / folder / "pkg/__init__.py").write_text("version = 42") - (tmp_path / "setup.cfg").write_text( - "[metadata]\nversion = attr: pkg.version\n" - + (tmp_path / "setup.cfg").read_text() - ) + + config = (tmp_path / "setup.cfg").read_text(encoding="utf-8") + overwrite = { + folder: {"pkg": {"__init__.py": "version = 42"}}, + "setup.cfg": "[metadata]\nversion = attr: pkg.version\n" + config, + } + jaraco.path.build(overwrite, prefix=tmp_path) dist = _get_dist(tmp_path, {}) assert dist.get_name() == "pkg" @@ -312,11 +314,16 @@ def test_setupcfg_metadata(self, tmp_path, folder, opts): def test_pyproject_metadata(self, tmp_path): _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {}) - (tmp_path / "src/pkg/__init__.py").write_text("version = 42") - (tmp_path / "pyproject.toml").write_text( - "[project]\nname = 'pkg'\ndynamic = ['version']\n" - "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n" - ) + + overwrite = { + "src": {"pkg": {"__init__.py": "version = 42"}}, + "pyproject.toml": ( + "[project]\nname = 'pkg'\ndynamic = ['version']\n" + "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n" + ), + } + jaraco.path.build(overwrite, prefix=tmp_path) + dist = _get_dist(tmp_path, {}) assert dist.get_version() == "42" assert dist.package_dir == {"": "src"} @@ -354,7 +361,7 @@ def _simulate_package_with_extension(self, tmp_path): ] setup(ext_modules=ext_modules) """ - (tmp_path / "setup.py").write_text(DALS(setup_script)) + (tmp_path / "setup.py").write_text(DALS(setup_script), encoding="utf-8") def test_skip_discovery_with_setupcfg_metadata(self, tmp_path): """Ensure that auto-discovery is not triggered when the project is based on @@ -367,14 +374,14 @@ def test_skip_discovery_with_setupcfg_metadata(self, tmp_path): requires = [] build-backend = 'setuptools.build_meta' """ - (tmp_path / "pyproject.toml").write_text(DALS(pyproject)) + (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") setupcfg = """ [metadata] name = proj version = 42 """ - (tmp_path / "setup.cfg").write_text(DALS(setupcfg)) + (tmp_path / "setup.cfg").write_text(DALS(setupcfg), encoding="utf-8") dist = _get_dist(tmp_path, {}) assert dist.get_name() == "proj" @@ -399,7 +406,7 @@ def test_dont_skip_discovery_with_pyproject_metadata(self, tmp_path): name = 'proj' version = '42' """ - (tmp_path / "pyproject.toml").write_text(DALS(pyproject)) + (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"): _get_dist(tmp_path, {}) @@ -416,7 +423,7 @@ def _simulate_package_with_data_files(self, tmp_path, src_root): manifest = """ global-include *.py *.txt """ - (tmp_path / "MANIFEST.in").write_text(DALS(manifest)) + (tmp_path / "MANIFEST.in").write_text(DALS(manifest), encoding="utf-8") EXAMPLE_SETUPCFG = """ [metadata] @@ -564,9 +571,12 @@ def _populate_project_dir(root, files, options): # NOTE: Currently pypa/build will refuse to build the project if no # `pyproject.toml` or `setup.py` is found. So it is impossible to do # completely "config-less" projects. - (root / "setup.py").write_text("import setuptools\nsetuptools.setup()") - (root / "README.md").write_text("# Example Package") - (root / "LICENSE").write_text("Copyright (c) 2018") + basic = { + "setup.py": "import setuptools\nsetuptools.setup()", + "README.md": "# Example Package", + "LICENSE": "Copyright (c) 2018", + } + jaraco.path.build(basic, prefix=root) _write_setupcfg(root, options) paths = (root / f for f in files) for path in paths: @@ -591,10 +601,10 @@ def _write_setupcfg(root, options): setupcfg["options"][key] = "\n" + str_value else: setupcfg["options"][key] = str(value) - with open(root / "setup.cfg", "w") as f: + with open(root / "setup.cfg", "w", encoding="utf-8") as f: setupcfg.write(f) print("~~~~~ setup.cfg ~~~~~") - print((root / "setup.cfg").read_text()) + print((root / "setup.cfg").read_text(encoding="utf-8")) def _run_build(path, *flags): diff --git a/setuptools/tests/test_dist_info.py b/setuptools/tests/test_dist_info.py index ad6cebad0b6..c6fe97e2bad 100644 --- a/setuptools/tests/test_dist_info.py +++ b/setuptools/tests/test_dist_info.py @@ -198,7 +198,8 @@ def run_command_inner(*cmd, **kwargs): "stderr": subprocess.STDOUT, "stdout": subprocess.PIPE, "text": True, - 'check': True, + "encoding": "utf-8", + "check": True, **kwargs, } cmd = [sys.executable, "-c", "__import__('setuptools').setup()", *map(str, cmd)] diff --git a/setuptools/tests/test_distutils_adoption.py b/setuptools/tests/test_distutils_adoption.py index eb7feba6373..74883d21998 100644 --- a/setuptools/tests/test_distutils_adoption.py +++ b/setuptools/tests/test_distutils_adoption.py @@ -8,6 +8,8 @@ IS_PYPY = '__pypy__' in sys.builtin_module_names +_TEXT_KWARGS = {"text": True, "encoding": "utf-8"} # For subprocess.run + def win_sr(env): """ @@ -24,7 +26,7 @@ def win_sr(env): def find_distutils(venv, imports='distutils', env=None, **kwargs): py_cmd = 'import {imports}; print(distutils.__file__)'.format(**locals()) cmd = ['python', '-c', py_cmd] - return venv.run(cmd, env=win_sr(env), text=True, **kwargs) + return venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS, **kwargs) def count_meta_path(venv, env=None): @@ -36,7 +38,7 @@ def count_meta_path(venv, env=None): """ ) cmd = ['python', '-c', py_cmd] - return int(venv.run(cmd, env=win_sr(env), text=True)) + return int(venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS)) skip_without_stdlib_distutils = pytest.mark.skipif( @@ -82,7 +84,7 @@ def test_pip_import(venv): Regression test for #3002. """ cmd = ['python', '-c', 'import pip'] - venv.run(cmd, text=True) + venv.run(cmd, **_TEXT_KWARGS) def test_distutils_has_origin(): @@ -130,7 +132,7 @@ def test_modules_are_not_duplicated_on_import( env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) script = ENSURE_IMPORTS_ARE_NOT_DUPLICATED.format(imported_module=imported_module) cmd = ['python', '-c', script] - output = venv.run(cmd, env=win_sr(env), text=True).strip() + output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() assert output == "success" @@ -154,5 +156,5 @@ def test_modules_are_not_duplicated_on_import( def test_log_module_is_not_duplicated_on_import(distutils_version, tmpdir_cwd, venv): env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version) cmd = ['python', '-c', ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED] - output = venv.run(cmd, env=win_sr(env), text=True).strip() + output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip() assert output == "success" diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index a001ae27c52..950cb23d211 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -467,7 +467,7 @@ def distutils_package(): 'from distutils.core import setup', ) with contexts.tempdir(cd=os.chdir): - with open('setup.py', 'w') as f: + with open('setup.py', 'w', encoding="utf-8") as f: f.write(distutils_setup_py) yield @@ -530,6 +530,7 @@ def test_setup_install_includes_dependencies(self, tmp_path, mock_index): stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, + encoding="utf-8", ) assert cp.returncode != 0 try: @@ -784,7 +785,7 @@ def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch): setup_attrs=dict(dependency_links=[]), ) test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') - with open(test_setup_cfg, 'w') as fp: + with open(test_setup_cfg, 'w', encoding="utf-8") as fp: fp.write( DALS( """ @@ -918,7 +919,7 @@ def test_setup_requires_with_find_links_in_setup_cfg( test_setup_py = os.path.join(test_pkg, 'setup.py') test_setup_cfg = os.path.join(test_pkg, 'setup.cfg') os.mkdir(test_pkg) - with open(test_setup_py, 'w') as fp: + with open(test_setup_py, 'w', encoding="utf-8") as fp: if with_dependency_links_in_setup_py: dependency_links = [os.path.join(temp_dir, 'links')] else: @@ -932,7 +933,7 @@ def test_setup_requires_with_find_links_in_setup_cfg( """ ).format(dependency_links=dependency_links) ) - with open(test_setup_cfg, 'w') as fp: + with open(test_setup_cfg, 'w', encoding="utf-8") as fp: fp.write( DALS( """ @@ -984,7 +985,7 @@ def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch): test_pkg = os.path.join(temp_dir, 'test_pkg') test_setup_py = os.path.join(test_pkg, 'setup.py') os.mkdir(test_pkg) - with open(test_setup_py, 'w') as fp: + with open(test_setup_py, 'w', encoding="utf-8") as fp: fp.write( DALS( """ @@ -1068,7 +1069,7 @@ class epcmd(build_py): test_pkg = os.path.join(temp_dir, 'test_pkg') test_setup_py = os.path.join(test_pkg, 'setup.py') os.mkdir(test_pkg) - with open(test_setup_py, 'w') as fp: + with open(test_setup_py, 'w', encoding="utf-8") as fp: fp.write( DALS( """ @@ -1244,7 +1245,7 @@ def create_setup_requires_package( ) else: test_setup_cfg_contents = '' - with open(os.path.join(test_pkg, 'setup.cfg'), 'w') as f: + with open(os.path.join(test_pkg, 'setup.cfg'), 'w', encoding="utf-8") as f: f.write(test_setup_cfg_contents) # setup.py @@ -1255,7 +1256,7 @@ def create_setup_requires_package( setuptools.setup(**%r) """ ) - with open(os.path.join(test_pkg, 'setup.py'), 'w') as f: + with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f: f.write(setup_py_template % test_setup_attrs) foobar_path = os.path.join(path, '%s-%s.tar.gz' % (distname, version)) diff --git a/setuptools/tests/test_editable_install.py b/setuptools/tests/test_editable_install.py index 862f8172cde..1df09fd256b 100644 --- a/setuptools/tests/test_editable_install.py +++ b/setuptools/tests/test_editable_install.py @@ -120,7 +120,7 @@ def editable_opts(request): @pytest.mark.parametrize( "files", [ - {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB}, # type: ignore + {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB}, EXAMPLE, # No setup.py script ], ) @@ -144,8 +144,8 @@ def test_editable_with_pyproject(tmp_path, venv, files, editable_opts): cmd = [venv.exe(), "-m", "mypkg"] assert subprocess.check_output(cmd).strip() == b"3.14159.post0 Hello World" - (project / "src/mypkg/data.txt").write_text("foobar") - (project / "src/mypkg/mod.py").write_text("x = 42") + (project / "src/mypkg/data.txt").write_text("foobar", encoding="utf-8") + (project / "src/mypkg/mod.py").write_text("x = 42", encoding="utf-8") assert subprocess.check_output(cmd).strip() == b"3.14159.post0 foobar 42" diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index af7d2f8295f..a4b0ecf3989 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -79,7 +79,8 @@ def run(): @staticmethod def _extract_mv_version(pkg_info_lines: List[str]) -> Tuple[int, int]: version_str = pkg_info_lines[0].split(' ')[1] - return tuple(map(int, version_str.split('.')[:2])) + major, minor = map(int, version_str.split('.')[:2]) + return major, minor def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env): """ @@ -93,7 +94,7 @@ def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env): ei.initialize_options() ei.save_version_info(setup_cfg) - with open(setup_cfg, 'r') as f: + with open(setup_cfg, 'r', encoding="utf-8") as f: content = f.read() assert '[egg_info]' in content @@ -138,7 +139,7 @@ def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env): ei.initialize_options() ei.save_version_info(setup_cfg) - with open(setup_cfg, 'r') as f: + with open(setup_cfg, 'r', encoding="utf-8") as f: content = f.read() assert '[egg_info]' in content @@ -250,7 +251,7 @@ def test_manifest_template_is_read(self, tmpdir_cwd, env): self._run_egg_info_command(tmpdir_cwd, env) egg_info_dir = os.path.join('.', 'foo.egg-info') sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt') - with open(sources_txt) as f: + with open(sources_txt, encoding="utf-8") as f: assert 'docs/usage.rst' in f.read().split('\n') def _setup_script_with_requires(self, requires, use_setup_cfg=False): @@ -491,7 +492,7 @@ def test_requires( egg_info_dir = os.path.join('.', 'foo.egg-info') requires_txt = os.path.join(egg_info_dir, 'requires.txt') if os.path.exists(requires_txt): - with open(requires_txt) as fp: + with open(requires_txt, encoding="utf-8") as fp: install_requires = fp.read() else: install_requires = '' @@ -537,8 +538,8 @@ def test_provides_extra(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'Provides-Extra: foobar' in pkg_info_lines assert 'Metadata-Version: 2.1' in pkg_info_lines @@ -556,8 +557,8 @@ def test_doesnt_provides_extra(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_text = pkginfo_file.read() + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_text = fp.read() assert 'Provides-Extra:' not in pkg_info_text @pytest.mark.parametrize( @@ -635,8 +636,7 @@ def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: - sources_text = sources_file.read() + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") if license_in_sources: assert 'LICENSE' in sources_text @@ -848,8 +848,8 @@ def test_setup_cfg_license_files( ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: - sources_lines = list(line.strip() for line in sources_file) + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") + sources_lines = [line.strip() for line in sources_text.splitlines()] for lf in incl_licenses: assert sources_lines.count(lf) == 1 @@ -1032,8 +1032,8 @@ def test_setup_cfg_license_file_license_files( ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file: - sources_lines = list(line.strip() for line in sources_file) + sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8") + sources_lines = [line.strip() for line in sources_text.splitlines()] for lf in incl_licenses: assert sources_lines.count(lf) == 1 @@ -1064,8 +1064,8 @@ def test_license_file_attr_pkg_info(self, tmpdir_cwd, env): pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') license_file_lines = [ line for line in pkg_info_lines if line.startswith('License-File:') ] @@ -1085,8 +1085,8 @@ def test_metadata_version(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') # Update metadata version if changed assert self._extract_mv_version(pkg_info_lines) == (2, 1) @@ -1111,8 +1111,8 @@ def test_long_description_content_type(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') expected_line = 'Description-Content-Type: text/markdown' assert expected_line in pkg_info_lines assert 'Metadata-Version: 2.1' in pkg_info_lines @@ -1132,8 +1132,8 @@ def test_long_description(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'Metadata-Version: 2.1' in pkg_info_lines assert '' == pkg_info_lines[-1] # last line should be empty long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :] @@ -1164,8 +1164,8 @@ def test_project_urls(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') expected_line = 'Project-URL: Link One, https://example.com/one/' assert expected_line in pkg_info_lines expected_line = 'Project-URL: Link Two, https://example.com/two/' @@ -1181,8 +1181,8 @@ def test_license(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'License: MIT' in pkg_info_lines def test_license_escape(self, tmpdir_cwd, env): @@ -1196,8 +1196,8 @@ def test_license_escape(self, tmpdir_cwd, env): data_stream=1, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'License: This is a long license text ' in pkg_info_lines assert ' over multiple lines' in pkg_info_lines @@ -1215,8 +1215,8 @@ def test_python_requires_egg_info(self, tmpdir_cwd, env): env=environ, ) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'Requires-Python: >=2.7.12' in pkg_info_lines assert self._extract_mv_version(pkg_info_lines) >= (1, 2) @@ -1239,7 +1239,7 @@ def test_egg_info_includes_setup_py(self, tmpdir_cwd): assert 'setup.py' in egg_info_instance.filelist.files - with open(egg_info_instance.egg_info + "/SOURCES.txt") as f: + with open(egg_info_instance.egg_info + "/SOURCES.txt", encoding="utf-8") as f: sources = f.read().split('\n') assert 'setup.py' in sources @@ -1276,8 +1276,8 @@ def test_egg_info_tag_only_once(self, tmpdir_cwd, env): }) self._run_egg_info_command(tmpdir_cwd, env) egg_info_dir = os.path.join('.', 'foo.egg-info') - with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: - pkg_info_lines = pkginfo_file.read().split('\n') + with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp: + pkg_info_lines = fp.read().split('\n') assert 'Version: 0.0.0.dev0' in pkg_info_lines diff --git a/setuptools/tests/test_find_packages.py b/setuptools/tests/test_find_packages.py index cb1900df3d5..4fefd3dccf5 100644 --- a/setuptools/tests/test_find_packages.py +++ b/setuptools/tests/test_find_packages.py @@ -72,8 +72,7 @@ def _mkdir(self, path, parent_dir=None): def _touch(self, path, dir_=None): if dir_: path = os.path.join(dir_, path) - fp = open(path, 'w') - fp.close() + open(path, 'wb').close() return path def test_regular_package(self): diff --git a/setuptools/tests/test_install_scripts.py b/setuptools/tests/test_install_scripts.py index a783459730a..595b7ade676 100644 --- a/setuptools/tests/test_install_scripts.py +++ b/setuptools/tests/test_install_scripts.py @@ -41,7 +41,7 @@ def test_sys_executable_escaping_unix(self, tmpdir, monkeypatch): monkeypatch.setattr('sys.executable', self.unix_exe) with tmpdir.as_cwd(): self._run_install_scripts(str(tmpdir)) - with open(str(tmpdir.join('foo')), 'r') as f: + with open(str(tmpdir.join('foo')), 'r', encoding="utf-8") as f: actual = f.readline() assert actual == expected @@ -55,7 +55,7 @@ def test_sys_executable_escaping_win32(self, tmpdir, monkeypatch): monkeypatch.setattr('sys.executable', self.win32_exe) with tmpdir.as_cwd(): self._run_install_scripts(str(tmpdir)) - with open(str(tmpdir.join('foo-script.py')), 'r') as f: + with open(str(tmpdir.join('foo-script.py')), 'r', encoding="utf-8") as f: actual = f.readline() assert actual == expected @@ -69,7 +69,7 @@ def test_executable_with_spaces_escaping_unix(self, tmpdir): expected = '#!%s\n' % self.unix_spaces_exe with tmpdir.as_cwd(): self._run_install_scripts(str(tmpdir), self.unix_spaces_exe) - with open(str(tmpdir.join('foo')), 'r') as f: + with open(str(tmpdir.join('foo')), 'r', encoding="utf-8") as f: actual = f.readline() assert actual == expected @@ -83,6 +83,6 @@ def test_executable_arg_escaping_win32(self, tmpdir): expected = '#!"%s"\n' % self.win32_exe with tmpdir.as_cwd(): self._run_install_scripts(str(tmpdir), '"' + self.win32_exe + '"') - with open(str(tmpdir.join('foo-script.py')), 'r') as f: + with open(str(tmpdir.join('foo-script.py')), 'r', encoding="utf-8") as f: actual = f.readline() assert actual == expected diff --git a/setuptools/tests/test_logging.py b/setuptools/tests/test_logging.py index 7a9a33f1ea7..cf89b3bd004 100644 --- a/setuptools/tests/test_logging.py +++ b/setuptools/tests/test_logging.py @@ -33,7 +33,7 @@ def test_verbosity_level(tmp_path, monkeypatch, flag, expected_level): assert logging.getLevelName(unset_log_level) == "NOTSET" setup_script = tmp_path / "setup.py" - setup_script.write_text(setup_py) + setup_script.write_text(setup_py, encoding="utf-8") dist = distutils.core.run_setup(setup_script, stop_after="init") dist.script_args = [flag, "sdist"] dist.parse_command_line() # <- where the log level is set diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index fbd21b19765..6911b0224c1 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -10,6 +10,7 @@ import logging from distutils import log from distutils.errors import DistutilsTemplateError +from typing import List, Tuple from setuptools.command.egg_info import FileList, egg_info, translate_pattern from setuptools.dist import Distribution @@ -53,7 +54,7 @@ def quiet(): def touch(filename): - open(filename, 'w').close() + open(filename, 'wb').close() # The set of files always in the manifest, including all files in the @@ -75,7 +76,7 @@ def touch(filename): ) -translate_specs = [ +translate_specs: List[Tuple[str, List[str], List[str]]] = [ ('foo', ['foo'], ['bar', 'foobar']), ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']), # Glob matching @@ -173,7 +174,7 @@ class TestManifestTest(TempDirTestCase): def setup_method(self, method): super().setup_method(method) - f = open(os.path.join(self.temp_dir, 'setup.py'), 'w') + f = open(os.path.join(self.temp_dir, 'setup.py'), 'w', encoding="utf-8") f.write(SETUP_PY) f.close() """ @@ -211,7 +212,8 @@ def setup_method(self, method): def make_manifest(self, contents): """Write a MANIFEST.in.""" - with open(os.path.join(self.temp_dir, 'MANIFEST.in'), 'w') as f: + manifest = os.path.join(self.temp_dir, 'MANIFEST.in') + with open(manifest, 'w', encoding="utf-8") as f: f.write(DALS(contents)) def get_files(self): @@ -368,7 +370,7 @@ def make_files(self, files): file = os.path.join(self.temp_dir, file) dirname, basename = os.path.split(file) os.makedirs(dirname, exist_ok=True) - open(file, 'w').close() + touch(file) def test_process_template_line(self): # testing all MANIFEST.in template patterns diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py index 41b96614f89..93474ae5af3 100644 --- a/setuptools/tests/test_packageindex.py +++ b/setuptools/tests/test_packageindex.py @@ -2,12 +2,12 @@ import urllib.request import urllib.error import http.client +from inspect import cleandoc from unittest import mock import pytest import setuptools.package_index -from .textwrap import DALS class TestPackageIndex: @@ -257,14 +257,15 @@ class TestPyPIConfig: def test_percent_in_password(self, tmp_home_dir): pypirc = tmp_home_dir / '.pypirc' pypirc.write_text( - DALS( + cleandoc( """ - [pypi] - repository=https://pypi.org - username=jaraco - password=pity% - """ - ) + [pypi] + repository=https://pypi.org + username=jaraco + password=pity% + """ + ), + encoding="utf-8", ) cfg = setuptools.package_index.PyPIConfig() cred = cfg.creds_by_repository['https://pypi.org'] diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py index 9b4937e2136..f666615d992 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -17,7 +17,7 @@ def test_devnull(self, tmpdir): @staticmethod def _file_writer(path): def do_write(): - with open(path, 'w') as f: + with open(path, 'w', encoding="utf-8") as f: f.write('xxx') return do_write @@ -114,7 +114,7 @@ def test_sandbox_violation_raised_hiding_setuptools(self, tmpdir): def write_file(): "Trigger a SandboxViolation by writing outside the sandbox" - with open('/etc/foo', 'w'): + with open('/etc/foo', 'w', encoding="utf-8"): pass with pytest.raises(setuptools.sandbox.SandboxViolation) as caught: @@ -126,8 +126,9 @@ def write_file(): cmd, args, kwargs = caught.value.args assert cmd == 'open' assert args == ('/etc/foo', 'w') - assert kwargs == {} + assert kwargs == {"encoding": "utf-8"} msg = str(caught.value) assert 'open' in msg assert "('/etc/foo', 'w')" in msg + assert "{'encoding': 'utf-8'}" in msg diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index 5d597709edb..387ec3bebff 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -10,7 +10,6 @@ import logging import distutils from inspect import cleandoc -from pathlib import Path from unittest import mock import pytest @@ -116,9 +115,7 @@ def latin1_fail(): def touch(path): - if isinstance(path, str): - path = Path(path) - path.write_text('', encoding='utf-8') + open(path, 'wb').close() return path @@ -386,7 +383,7 @@ def test_setup_py_missing(self): assert 'setup.py' not in manifest def test_setup_py_excluded(self): - with open("MANIFEST.in", "w") as manifest_file: + with open("MANIFEST.in", "w", encoding="utf-8") as manifest_file: manifest_file.write("exclude setup.py") dist = Distribution(SETUP_ATTRS) @@ -441,7 +438,7 @@ def test_manifest_is_written_with_utf8_encoding(self): filename = os.path.join('sdist_test', 'smörbröd.py') # Must create the file or it will get stripped. - open(filename, 'w').close() + touch(filename) # Add UTF-8 filename and write manifest with quiet(): @@ -469,7 +466,7 @@ def test_write_manifest_allows_utf8_filenames(self): filename = os.path.join(b'sdist_test', Filenames.utf_8) # Must touch the file or risk removal - open(filename, "w").close() + touch(filename) # Add filename and write manifest with quiet(): @@ -546,7 +543,7 @@ def test_manifest_is_read_with_utf8_encoding(self): manifest.close() # The file must exist to be included in the filelist - open(filename, 'w').close() + touch(filename) # Re-read manifest cmd.filelist.files = [] @@ -577,7 +574,7 @@ def test_read_manifest_skips_non_utf8_filenames(self): manifest.close() # The file must exist to be included in the filelist - open(filename, 'w').close() + touch(filename) # Re-read manifest cmd.filelist.files = [] @@ -598,7 +595,7 @@ def test_sdist_with_utf8_encoded_filename(self): cmd.ensure_finalized() filename = os.path.join(b'sdist_test', Filenames.utf_8) - open(filename, 'w').close() + touch(filename) with quiet(): cmd.run() @@ -639,7 +636,7 @@ def test_sdist_with_latin1_encoded_filename(self): # Latin-1 filename filename = os.path.join(b'sdist_test', Filenames.latin_1) - open(filename, 'w').close() + touch(filename) assert os.path.isfile(filename) with quiet(): @@ -736,7 +733,7 @@ def test_pyproject_toml_excluded(self, source_dir): Check that pyproject.toml can excluded even if present """ touch(source_dir / 'pyproject.toml') - with open('MANIFEST.in', 'w') as mts: + with open('MANIFEST.in', 'w', encoding="utf-8") as mts: print('exclude pyproject.toml', file=mts) dist = Distribution(SETUP_ATTRS) dist.script_name = 'setup.py'